From 9ddbeef937d1a5c786600ac3ce1ea24b1c72890f Mon Sep 17 00:00:00 2001 From: Morgan Caron Date: Wed, 21 Oct 2026 04:07:07 +0200 Subject: [PATCH] feat(unittest)!: modernize test suite builders, event watching, and fluent assertions --- .clang-format | 2 +- .github/workflows/ci-cpp-linux.yml | 13 +- .github/workflows/ci-cpp-macos.yml | 13 +- .github/workflows/ci-cpp-windows.yml | 13 +- .gitignore | 4 +- README.md | 9 +- docs/Doxyfile | 1 + docs/astro.config.mjs | 30 +- docs/package-lock.json | 2741 ++++++++--------- docs/package.json | 2 +- docs/scripts/generate-api.mjs | 1266 +++++--- .../components/TableOfContentsProgress.astro | 169 +- docs/src/content/docs/en/guides/lockers.mdx | 55 +- docs/src/content/docs/en/guides/unit-test.mdx | 1572 ++++++++++ docs/src/content/docs/en/index.mdx | 53 +- docs/src/content/docs/fr/guides/lockers.mdx | 55 +- docs/src/content/docs/fr/guides/unit-test.mdx | 1572 ++++++++++ docs/src/content/docs/fr/index.mdx | 53 +- docs/src/plugins/remark-cppreference.mjs | 4 +- docs/src/plugins/remark-cpputils.mjs | 59 + docs/src/plugins/remark-terminal.mjs | 163 + docs/src/styles/components.css | 245 ++ docs/src/styles/custom.css | 10 + docs/src/styles/layout.css | 59 +- docs/src/styles/reference.css | 241 +- include/Stl/expected.hpp | 10 +- modules/Container/MultiKeyMap.mpp | 2 +- modules/Container/NetworkPtr.mpp | 58 +- modules/Container/Sequence.mpp | 118 +- modules/Execution/DoNotOptimize.mpp | 26 + modules/Execution/Execution.mpp | 1 + modules/FileSystem/Directory.mpp | 3 +- modules/Language/INI/IniLexer.mpp | 3 +- modules/Language/Language.mpp | 1 + modules/Language/Lexer/AstLexer.mpp | 9 +- modules/Language/Lexer/Engine.mpp | 3 +- .../Language/Parameters/ParametersLexer.mpp | 46 +- modules/Language/Parser/AstParser.mpp | 5 +- modules/Language/XML/Flow.mpp | 2 +- modules/Language/XML/Utility.mpp | 33 + modules/Language/XML/XmlLexer.mpp | 3 +- modules/Log/Config/CppUtils.mpp | 12 +- modules/Log/Config/UnitTests.mpp | 17 +- modules/Log/Logger.mpp | 6 + modules/Math/Random.mpp | 220 ++ modules/Math/Utility.mpp | 28 + modules/Pattern/Multiton.mpp | 22 +- modules/String/Concept.mpp | 90 +- modules/String/Hash.mpp | 3 +- modules/String/HashTable.mpp | 7 +- modules/String/Utility.mpp | 121 +- modules/System/Pipe.mpp | 207 +- modules/System/Process.mpp | 396 +++ modules/System/Sandbox.mpp | 176 ++ modules/System/System.mpp | 2 + modules/Terminal/Canvas.mpp | 5 +- modules/Terminal/TextModifier.mpp | 2 +- modules/Thread/SharedLocker.mpp | 4 +- modules/Type/Concept.mpp | 16 + modules/Type/Hash.mpp | 6 +- modules/UnitTest/Assertion.mpp | 155 + modules/UnitTest/Benchmark.mpp | 65 + modules/UnitTest/EventWatcher.mpp | 201 ++ modules/UnitTest/Expectation.mpp | 957 ++++++ modules/UnitTest/JUnitFormatter.mpp | 147 + modules/UnitTest/LifetimeChecker.mpp | 24 + modules/UnitTest/Report.mpp | 33 + modules/UnitTest/Runner.mpp | 597 ++++ modules/UnitTest/UnitTest.mpp | 1077 ++++--- tests/Chrono/Chronometer.mpp | 10 +- tests/Container/BTree.mpp | 102 +- tests/Container/BidirectionalMap.mpp | 102 +- tests/Container/DependencyGraph.mpp | 124 +- tests/Container/FlatHashMap.mpp | 315 +- tests/Container/MeshNetwork.mpp | 398 +-- tests/Container/MultiKeyMap.mpp | 364 +-- tests/Container/NetworkPtr.mpp | 156 +- tests/Container/Pair.mpp | 12 +- tests/Container/SafeShared.mpp | 158 +- tests/Container/Sequence.mpp | 45 +- tests/Container/Stack.mpp | 100 +- tests/Container/Tree.mpp | 20 +- tests/Container/TypedStack.mpp | 72 +- tests/Container/Vector.mpp | 10 +- tests/Crypto/HMAC.mpp | 16 +- tests/Crypto/SHA256.mpp | 28 +- tests/Execution/Event.mpp | 52 +- tests/Execution/EventDispatcher.mpp | 125 +- tests/Execution/EventQueue.mpp | 46 +- tests/Execution/Pipeline.mpp | 104 +- tests/Execution/Retry.mpp | 86 +- tests/Execution/ScopeGuard.mpp | 16 +- tests/Execution/TryExecute.mpp | 36 +- tests/FileSystem/Directory.mpp | 14 +- tests/FileSystem/File.mpp | 116 +- tests/FileSystem/FileRange.mpp | 44 +- tests/FileSystem/FileStaging.mpp | 101 +- tests/FileSystem/IndexedStorage.mpp | 94 +- tests/FileSystem/MeshPager.mpp | 74 +- tests/FileSystem/Watcher.mpp | 135 +- tests/Language/ASTParser.mpp | 134 +- tests/Language/CLikeCompiler.mpp | 13 +- tests/Language/CSV/CSVLexer.mpp | 88 +- tests/Language/CSV/Mapping.mpp | 126 +- tests/Language/CSV/Parsing.mpp | 34 +- tests/Language/CSV/Range.mpp | 16 +- tests/Language/CallStackCompiler.mpp | 31 +- tests/Language/GrammarParser.mpp | 13 +- tests/Language/INI/IniLexer.mpp | 34 +- tests/Language/JSON/JsonLexer.mpp | 114 +- tests/Language/JSON/Mapping.mpp | 50 +- tests/Language/JSON/Parsing.mpp | 74 +- tests/Language/LabelsCompiler.mpp | 55 +- .../Lexer/Grammar/HighLevelGrammar.mpp | 168 +- .../Lexer/Grammar/LowLevelGrammar.mpp | 176 +- tests/Language/Markdown/MarkdownLexer.mpp | 104 +- tests/Language/MetaCircularParser.mpp | 61 +- tests/Language/MetaCircularVirtualMachine.mpp | 577 ++-- tests/Language/Parameters/ParametersLexer.mpp | 52 +- tests/Language/VirtualMachine.mpp | 207 +- tests/Language/XML/FlowRunner.mpp | 54 +- tests/Language/XML/Pipeline.mpp | 329 +- tests/Language/XML/Utility.mpp | 15 + tests/Language/XML/XmlLexer.mpp | 68 +- tests/Log/ChronoLogger.mpp | 12 +- tests/Log/FileSink.mpp | 16 +- tests/Log/LogRotate.mpp | 46 +- tests/Log/Logger.mpp | 56 +- tests/Math/BigInt.mpp | 132 +- tests/Math/Endian.mpp | 18 +- tests/Math/Random.mpp | 64 +- tests/Memory/Memory.mpp | 10 +- tests/Network/Client.mpp | 8 +- tests/Network/DNS.mpp | 28 +- tests/Network/Http.mpp | 28 +- tests/Network/Server.mpp | 40 +- tests/Network/Socket.mpp | 294 +- tests/Pattern/Pattern.mpp | 26 +- tests/Ranges/Drain.mpp | 20 +- tests/Ranges/Expected.mpp | 80 +- tests/Ranges/Inspect.mpp | 40 +- tests/Ranges/Parallel.mpp | 121 +- tests/Stl/format.mpp | 10 +- tests/String/Concept.mpp | 72 + tests/String/Encoding.mpp | 66 +- tests/String/Utility.mpp | 193 +- tests/System/Error.mpp | 56 +- tests/System/Pipe.mpp | 46 + tests/System/Process.mpp | 29 + tests/System/Sandbox.mpp | 73 + tests/Terminal/AutoScroll.mpp | 28 +- tests/Terminal/Button.mpp | 58 +- tests/Terminal/Canvas.mpp | 44 +- tests/Terminal/Element.mpp | 28 +- tests/Terminal/Layout.mpp | 40 +- tests/Terminal/Mouse.mpp | 8 +- tests/Terminal/Primitive.mpp | 130 +- tests/Terminal/ProgressBar.mpp | 16 +- tests/Terminal/RawTerminal.mpp | 86 +- tests/Terminal/Scrollable.mpp | 46 +- tests/Terminal/Terminal.mpp | 24 +- tests/Terminal/Text.mpp | 46 +- tests/Terminal/Title.mpp | 12 +- tests/Terminal/Utility.mpp | 56 +- tests/Thread/AsyncEventDispatcher.mpp | 38 +- tests/Thread/Poll.mpp | 36 +- tests/Thread/ScheduledEventDispatcher.mpp | 28 +- tests/Thread/Scheduler.mpp | 165 +- tests/Thread/SharedLocker.mpp | 60 +- tests/Thread/SharedPtr.mpp | 180 +- tests/Thread/ThreadLoop.mpp | 36 +- tests/Thread/ThreadPool.mpp | 66 +- tests/Thread/TryAsync.mpp | 34 +- tests/Thread/UniqueLocker.mpp | 62 +- tests/Type/Concept.mpp | 75 +- tests/Type/Enum.mpp | 30 +- tests/Type/Mapping.mpp | 96 +- tests/Type/ReadWriteAdapter.mpp | 90 +- tests/Type/Serializer.mpp | 80 +- tests/Type/Tuple.mpp | 10 +- tests/UnitTest/JUnitFormatter.mpp | 47 + tests/UnitTest/LifetimeChecker.mpp | 76 +- tests/UnitTest/UnitTest.mpp | 805 ++++- tests/UnitTests.mpp | 6 + tests/main.cpp | 10 +- 185 files changed, 16292 insertions(+), 6935 deletions(-) create mode 100644 docs/src/content/docs/en/guides/unit-test.mdx create mode 100644 docs/src/content/docs/fr/guides/unit-test.mdx create mode 100644 docs/src/plugins/remark-cpputils.mjs create mode 100644 docs/src/plugins/remark-terminal.mjs create mode 100644 modules/Execution/DoNotOptimize.mpp create mode 100644 modules/Language/XML/Utility.mpp create mode 100644 modules/System/Process.mpp create mode 100644 modules/System/Sandbox.mpp create mode 100644 modules/UnitTest/Assertion.mpp create mode 100644 modules/UnitTest/Benchmark.mpp create mode 100644 modules/UnitTest/EventWatcher.mpp create mode 100644 modules/UnitTest/Expectation.mpp create mode 100644 modules/UnitTest/JUnitFormatter.mpp create mode 100644 modules/UnitTest/Report.mpp create mode 100644 modules/UnitTest/Runner.mpp create mode 100644 tests/Language/XML/Utility.mpp create mode 100644 tests/String/Concept.mpp create mode 100644 tests/System/Pipe.mpp create mode 100644 tests/System/Process.mpp create mode 100644 tests/System/Sandbox.mpp create mode 100644 tests/UnitTest/JUnitFormatter.mpp diff --git a/.clang-format b/.clang-format index 24b13899..7c6403b8 100644 --- a/.clang-format +++ b/.clang-format @@ -115,7 +115,7 @@ AlignTrailingComments: Kind: Never # Accolades -RemoveBracesLLVM: true +RemoveBracesLLVM: false # Order SortIncludes: CaseInsensitive diff --git a/.github/workflows/ci-cpp-linux.yml b/.github/workflows/ci-cpp-linux.yml index 645b4bd2..7181d88d 100644 --- a/.github/workflows/ci-cpp-linux.yml +++ b/.github/workflows/ci-cpp-linux.yml @@ -15,6 +15,11 @@ jobs: build: name: Ubuntu (${{ matrix.compiler.name }}) runs-on: ubuntu-latest + permissions: + contents: read + issues: read + checks: write + pull-requests: write strategy: fail-fast: false matrix: @@ -71,4 +76,10 @@ jobs: - name: Run tests run: | - xmake run + xmake run CppUtils-UnitTests --report=junit:build/test-results.xml + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: "**/test-results.xml" diff --git a/.github/workflows/ci-cpp-macos.yml b/.github/workflows/ci-cpp-macos.yml index 57a2dbbe..7216dbbe 100644 --- a/.github/workflows/ci-cpp-macos.yml +++ b/.github/workflows/ci-cpp-macos.yml @@ -15,6 +15,11 @@ jobs: build: name: MacOS (${{ matrix.compiler.name }}) runs-on: macos-latest + permissions: + contents: read + issues: read + checks: write + pull-requests: write strategy: fail-fast: false matrix: @@ -48,4 +53,10 @@ jobs: - name: Run tests run: | - xmake run + xmake run CppUtils-UnitTests --report=junit:build/test-results.xml + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/macos@v2 + if: always() + with: + files: "**/test-results.xml" diff --git a/.github/workflows/ci-cpp-windows.yml b/.github/workflows/ci-cpp-windows.yml index 437104ae..5573b1af 100644 --- a/.github/workflows/ci-cpp-windows.yml +++ b/.github/workflows/ci-cpp-windows.yml @@ -15,6 +15,11 @@ jobs: build: name: Windows (${{ matrix.compiler.name }}) runs-on: windows-latest + permissions: + contents: read + issues: read + checks: write + pull-requests: write strategy: fail-fast: false matrix: @@ -52,5 +57,11 @@ jobs: - name: Run tests shell: pwsh run: | - xmake run + xmake run CppUtils-UnitTests --report=junit:build/test-results.xml + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/windows@v2 + if: always() + with: + files: "**/test-results.xml" diff --git a/.gitignore b/.gitignore index 5983c3cd..1dda725f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ /docs/dist /docs/.astro /docs/doxygen_xml +/docs/html +/docs/latex /docs/src/content/docs/**/reference/ /docs/src/data/cppreference-symbols.json +/docs/src/data/cpputils-symbols.json /docs/.starlight-icons - diff --git a/README.md b/README.md index b2e51980..1251686c 100644 --- a/README.md +++ b/README.md @@ -179,8 +179,13 @@ - [`Variant`](modules/Type/Variant.mpp) - Generic print and comparison operators for `std::variant` ### 🧪 Unit Testing -- [`LifetimeChecker`](modules/UnitTest/LifetimeChecker.mpp) - Object that prints construction, destruction, copy and move operations, while counting copies and moves for test verification -- [`UnitTest`](modules/UnitTest/UnitTest.mpp) - Minimal test framework with assertions and filters +- [`Benchmark`](modules/UnitTest/Benchmark.mpp) - Benchmarking utilities with execution timing statistics and performance comparisons +- [`EventWatcher`](modules/UnitTest/EventWatcher.mpp) - Event observation and expectation verification for event-driven systems +- [`Expectation`](modules/UnitTest/Expectation.mpp) - Fluent assertion API (`expect(a) == b`, `throws`, `noThrow`, `crashes`, `eventually`, `completes`) +- [`JUnitFormatter`](modules/UnitTest/JUnitFormatter.mpp) - JUnit XML report generator for CI/CD test reporting +- [`LifetimeChecker`](modules/UnitTest/LifetimeChecker.mpp) - Diagnostic probe tracking object construction, destruction, copy and move counts +- [`Runner`](modules/UnitTest/Runner.mpp) - Test runner engine with process isolation, timeout enforcement, and dependency execution +- [`UnitTest`](modules/UnitTest/UnitTest.mpp) - Modern macro-free unit testing framework with declarative suite builders (`testSuite`), parameterized/typed suites, and compile-time test execution ### 🧩 Miscellaneous - Many other functions diff --git a/docs/Doxyfile b/docs/Doxyfile index f08d14e9..454fa0d8 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -14,3 +14,4 @@ EXTRACT_STATIC = YES EXTRACT_PRIVATE = NO QUIET = YES WARNINGS = NO +PREDEFINED = OS_LINUX diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 8945e646..437b4eef 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -12,12 +12,14 @@ import { starlightIconsPlugin, starlightIconsIntegration } from 'starlight-plugi import UnoCSS from 'unocss/astro'; import mermaid from 'astro-mermaid'; import { remarkCppReference } from './src/plugins/remark-cppreference.mjs'; +import { remarkCppUtils } from './src/plugins/remark-cpputils.mjs'; +import { remarkTerminal } from './src/plugins/remark-terminal.mjs'; export default defineConfig({ site: 'https://morgancaron.github.io', base: process.env.GITHUB_ACTIONS === 'true' ? '/CppUtils' : '/', markdown: { - remarkPlugins: [remarkCppReference], + remarkPlugins: [remarkCppReference, remarkCppUtils, remarkTerminal], }, integrations: [ starlight({ @@ -32,6 +34,8 @@ export default defineConfig({ fr: 'Retour en haut', }, showTooltip: true, + borderRadius: '50', + svgStrokeWidth: 2, }), starlightAnnouncement({ displayMode: 'rotate', @@ -141,18 +145,32 @@ gtag('config', 'G-9EYYYD173T', { slug: 'guides/build-configuration', }, { - label: 'RAII synchronization (Lockers)', - translations: { fr: 'Synchronisation RAII (Lockers)' }, + label: 'Unit testing framework', + translations: { fr: 'Tests unitaires' }, + slug: 'guides/unit-test', + }, + { + label: 'RAII Lockers', + translations: { fr: 'Verrous RAII' }, slug: 'guides/lockers', }, ], }, { - label: 'API reference', + label: 'CppUtils API reference', translations: { - fr: 'Référence d’API', + fr: 'Référence d’API CppUtils', }, - items: [{ autogenerate: { directory: 'reference', collapsed: true } }], + items: [ + { + label: 'CppUtils API reference', + translations: { + fr: 'Référence d’API CppUtils', + }, + slug: 'reference', + }, + { autogenerate: { directory: 'reference', collapsed: true } }, + ], }, ], }), diff --git a/docs/package-lock.json b/docs/package-lock.json index 1bae5d0a..50f016b4 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -15,7 +15,7 @@ "astro": "^7.2.10", "astro-contributors": "^0.10.0", "astro-mermaid": "^2.1.0", - "mermaid": "^11.17.2", + "mermaid": "^12.2.0", "sharp": "^0.35.3", "starlight-announcement": "^2.0.0", "starlight-base-path": "^0.2.1", @@ -41,30 +41,30 @@ } }, "node_modules/@astrojs/compiler-binding": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.4.1.tgz", - "integrity": "sha512-ZYVDW1P58OXyxqZbOMp+/2U9KA7y2esIx9r4pWdpZ/l6fCZhaw4ul+D7EgpRVxYLkyTWqzANbTTUFGPFP8zDHA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.5.2.tgz", + "integrity": "sha512-lEn76+0JcMQ5RDXU0wSMWJg2ZppiZvSULpghC29cfv4o9bhvWFDbQjsCtvjjXN9iVLFQ7fL3wTR2L2UU13mP8g==", "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@astrojs/compiler-binding-android-arm64": "0.4.1", - "@astrojs/compiler-binding-darwin-arm64": "0.4.1", - "@astrojs/compiler-binding-darwin-x64": "0.4.1", - "@astrojs/compiler-binding-linux-arm64-gnu": "0.4.1", - "@astrojs/compiler-binding-linux-arm64-musl": "0.4.1", - "@astrojs/compiler-binding-linux-x64-gnu": "0.4.1", - "@astrojs/compiler-binding-linux-x64-musl": "0.4.1", - "@astrojs/compiler-binding-wasm32-wasi": "0.4.1", - "@astrojs/compiler-binding-win32-arm64-msvc": "0.4.1", - "@astrojs/compiler-binding-win32-x64-msvc": "0.4.1" + "@astrojs/compiler-binding-android-arm64": "0.5.2", + "@astrojs/compiler-binding-darwin-arm64": "0.5.2", + "@astrojs/compiler-binding-darwin-x64": "0.5.2", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.5.2", + "@astrojs/compiler-binding-linux-arm64-musl": "0.5.2", + "@astrojs/compiler-binding-linux-x64-gnu": "0.5.2", + "@astrojs/compiler-binding-linux-x64-musl": "0.5.2", + "@astrojs/compiler-binding-wasm32-wasi": "0.5.2", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.5.2", + "@astrojs/compiler-binding-win32-x64-msvc": "0.5.2" } }, "node_modules/@astrojs/compiler-binding-android-arm64": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.4.1.tgz", - "integrity": "sha512-Wtx2DZORuNTLASLLZ8hlrjFaJe1LUloatcCn9EPq7knBhRpjMGXeJANIJMeBZG4igvz7x+3nyAJM2iwCSW9xog==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.5.2.tgz", + "integrity": "sha512-kckIxon6Rm/Bxzh9mGPYpRRqUbToUQoHptCmpMmE+Yt0Xumq8CqrKUidyYhrBsrHVO7bxuRzzWUUqvpf+iYceQ==", "cpu": [ "arm64" ], @@ -78,9 +78,9 @@ } }, "node_modules/@astrojs/compiler-binding-darwin-arm64": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.4.1.tgz", - "integrity": "sha512-7iwXcU+hB60Y1SvOfnWHNioT72gDsDQKaDipXh2Onwp/TvQMozhbvhy13wJR8Af940a0h6PbddCeJFpj791UDA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.5.2.tgz", + "integrity": "sha512-1ZG5Ar3dD8HtodX9XH1mRmE0qBXZlvjYXSFfpgFwocqfWnvhFuqtGUNseL8wgfImiRMztIjpQqlogQFQQtgiag==", "cpu": [ "arm64" ], @@ -94,9 +94,9 @@ } }, "node_modules/@astrojs/compiler-binding-darwin-x64": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.4.1.tgz", - "integrity": "sha512-pifjICos49RXhSVKSDu5gDB0/8j0O7DmxSp1DaWW2PyLG5ekwSkcrfOZto5JqhdI7QYMZB3XOiXpsdOXpS4npA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.5.2.tgz", + "integrity": "sha512-+kl35S7hiHJ2cb7eujwEsPFYYuSgE11uEE2Z20lAJ+undpgulMcW+weQqFPOSSZccEKHOiLIY+kQHkb4e4092w==", "cpu": [ "x64" ], @@ -110,9 +110,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.4.1.tgz", - "integrity": "sha512-hsZMlNWc6LqWHh9LlSwTq1/XCY7pxA5rkkKIdZRV8mgbOz9OwLvjEoe5Jry13eIZjMUQO8XnQyzk8o/Pjw2YFA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.5.2.tgz", + "integrity": "sha512-BqlR0EHNQLPLwgL9+xogjsAc7zGf0cHho2CPdhv0P1yqbVMwGVaGAqrANP6VYAXCqSC43AF4Czcxc2n+oqdesg==", "cpu": [ "arm64" ], @@ -126,9 +126,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.4.1.tgz", - "integrity": "sha512-a3EW27f+Z9uf57vYmpLgUriA1y9wDhKz57PT8D8XlFZt2HZEWhUxKuxI1jvKVI9p8okYee11WXEm/FHY1G9yoQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.5.2.tgz", + "integrity": "sha512-GRGyby3AZWw5d6JQzidyxPyvGgHWsSusrpgKyFVpDTKctABF7L3Pd5V1cRJbV36/rcu+Eh292/wL7pF7MkmGkQ==", "cpu": [ "arm64" ], @@ -142,9 +142,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.4.1.tgz", - "integrity": "sha512-XruPTKB/SS4DFzeAjQ1ZyM0ieHDjapHkOSnnzRApZ4FXY2YAI7UY3mETjfJiIS/acyZyrnPH5jObv1gATTdUJQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.5.2.tgz", + "integrity": "sha512-9XAqGTMQAtIslMsjK3xdyY6OI9d59OZqcLBB6pvD/DcoR0daYswfrKx2azdDERDY4AqAnWPC7odVM1BmxYv3Vw==", "cpu": [ "x64" ], @@ -158,9 +158,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-x64-musl": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.4.1.tgz", - "integrity": "sha512-E8j7lGanqNpudtkUbsFaOLJhmwFe4Ecqux7K9hkj4wULIPNZRM2bYZNcMSj8r1oWvaUiwZl8LaaNwv3+14jqhw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.5.2.tgz", + "integrity": "sha512-jOViewGeTh9bSfS8vK72ECxoy2yvmA+Pd/ZcEvBI5c7+7IcfHwZ5qA4L9fRYuOhSrAifvQ2dV4vm1XAtloaWBw==", "cpu": [ "x64" ], @@ -174,25 +174,27 @@ } }, "node_modules/@astrojs/compiler-binding-wasm32-wasi": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.4.1.tgz", - "integrity": "sha512-vNLxIv41ZE+nmPFtw24cRW5ZXG+ZPcAGtgigtjCP8lvcxKI7u2GrXoMsCgEH5fMw8WCEJpwLvsVBg15rJKAkyA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.5.2.tgz", + "integrity": "sha512-Z7p64doar1uvR5C9Xs6j1CV9lGlPVQX7OQ2aAtZ/xKC5aGS6GB13nyc2UfQXTQLvZrvVwsyaflTQy6ziY5Bekw==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.2.4" + "@emnapi/core": "1.11.3", + "@emnapi/runtime": "1.11.3", + "@napi-rs/wasm-runtime": "^1.2.5" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.4.1.tgz", - "integrity": "sha512-/xTDgRsFDoTh0/+KFXJ74DY4Jqn8CoSHwa8UP5RAblHfnRHkenwfSl4zYDET61O9vXKLk09UzHjjq09yP20/AQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.5.2.tgz", + "integrity": "sha512-Tr13bh+ahTWsjXjzctK7pFpWV+dUU4FtBfETlR7gO+ueex0F+RDch7eRr0YfKysaRhk9tWm14lsCJ5uWyFPNHQ==", "cpu": [ "arm64" ], @@ -206,9 +208,9 @@ } }, "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.4.1.tgz", - "integrity": "sha512-0x5J6iHZO0Fjo/CVzoIbXKzWnGESftDbEUQ/oXQ61HhUAFoR7+T1jfToZSNAS+gYyBXHSTpMWYC7lzarbhClcg==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.5.2.tgz", + "integrity": "sha512-SfKkI+jOQYLh6wojPvVPV0lFfQnxisweN9B+qCzRvZp7DHAuGif8Y0WcC5PUgN4jb8VKy57LggQDtggR46Du1g==", "cpu": [ "x64" ], @@ -222,43 +224,43 @@ } }, "node_modules/@astrojs/compiler-rs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.4.1.tgz", - "integrity": "sha512-//NmAuRhy7eU9iP0ceI5Pymy55M+nUZ//kS6XeOyhBWdDib2KbJOyFaVJ52k7sDkJwTKVox7oPu3SpYiVnGm5A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.5.2.tgz", + "integrity": "sha512-IvwYLB8t1M86rKO4oPdBi9GP4gzXcgrVlNgocjwos1jeeUuEIV8d9Crn1PvYD/5IKteWOxpIHomf7e8iYlkE+Q==", "license": "MIT", "dependencies": { - "@astrojs/compiler-binding": "0.4.1" + "@astrojs/compiler-binding": "0.5.2" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@astrojs/internal-helpers": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.11.0.tgz", - "integrity": "sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.13.0.tgz", + "integrity": "sha512-20jxQqnSmpskcGyad7N5l6Xf1gz0UCa6lNde44bEBdaXGpncEYfAZJ142r2ZgzBaG4dUyzGmhqZWv1INU2eiHQ==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.4", + "@types/hast": "^3.0.5", "@types/mdast": "^4.0.4", - "js-yaml": "^4.3.0", - "picomatch": "^4.0.4", + "picomatch": "^4.0.7", "retext-smartypants": "^6.2.0", - "shiki": "^4.0.2", - "smol-toml": "^1.6.0", - "unified": "^11.0.5" + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "unified": "^11.0.5", + "yaml": "^2.9.1" } }, "node_modules/@astrojs/markdown-remark": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.3.1.tgz", - "integrity": "sha512-QCjsjZQl0+Cgb91+T3Ip9hyUe1r5aLm4udi584rML9YO3syupbAYE+jWpAxYg+z4lFLNUugPrylFxZ+8qFTClg==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.3.4.tgz", + "integrity": "sha512-AB8S4EQKSryzH624iMDtvC+1AD5nSPzYpCXxc7e1H5ydXhv9B5Td2QfCPA0JyEwqsJpsIsPlW0iOqiy1imdqYw==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.11.0", - "@astrojs/prism": "4.0.2", + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/prism": "4.1.1", "@mdx-js/mdx": "^3.1.1", - "acorn": "^8.16.0", + "acorn": "^8.19.0", "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", @@ -270,8 +272,8 @@ "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", - "remark-smartypants": "^3.0.2", - "source-map": "^0.7.6", + "remark-smartypants": "^3.0.3", + "source-map": "^0.8.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", @@ -280,34 +282,34 @@ } }, "node_modules/@astrojs/markdown-satteri": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.1.tgz", - "integrity": "sha512-EniHbFNa6SQHDih7JnpPos3NWXO6hdt4raBcOosfGmlfc3gP9CI/sESA3VOf1PgJZ7SFfewlZW9Livn0JugEZg==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.5.tgz", + "integrity": "sha512-0IeGdoFbv0f+442KJhYS44Wg8F3MuHmRsxIEYuFyqkjRubc6dPBFZ7azor5t4AeXLQoT5A5h9HyViKpd70Dl9w==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.11.0", - "@astrojs/prism": "4.0.2", + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/prism": "4.1.1", "github-slugger": "^2.0.0", - "satteri": "^0.10.3" + "satteri": "^0.10.5" } }, "node_modules/@astrojs/mdx": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.0.1.tgz", - "integrity": "sha512-VKodp/f3+XE6L0950K+ZOAolY47vdt0R78Ao+L2RdJ1hfAdYffL7IEjc/MsoyuS7CPVvCsd5/I3RP0ZyAIewdQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.1.0.tgz", + "integrity": "sha512-xzrlXrjpBYvC27MTkP+2NMiJG4Jjl2DcoNc/EF9RNxAp0dwhvXC3b22+7OO4kHJQKfISk2uy38HYp1UxX7JfSg==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.11.0", - "@astrojs/markdown-satteri": "0.4.1", - "es-module-lexer": "^2.0.0" + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/markdown-satteri": "0.4.4", + "es-module-lexer": "^3.0.3" }, "engines": { - "node": ">=22.12.0" + "node": ">=22.22.0" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.3.0", "@astrojs/markdown-satteri": "^0.4.0", - "astro": "^7.2.6" + "astro": "^7.2.10" }, "peerDependenciesMeta": { "@astrojs/markdown-remark": { @@ -315,16 +317,40 @@ } } }, + "node_modules/@astrojs/mdx/node_modules/@astrojs/markdown-satteri": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.4.tgz", + "integrity": "sha512-Byhe9CAiGFJrhMACKpgN/FPf4lcwapqwFQAB7E1sPELTEJINfBIpgnEYOBTXzr//fvSSNhfp/X3AqN4mUV9p+A==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/prism": "4.1.0", + "github-slugger": "^2.0.0", + "satteri": "^0.10.5" + } + }, + "node_modules/@astrojs/mdx/node_modules/@astrojs/prism": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.1.0.tgz", + "integrity": "sha512-zvRaL3MKTAXWfVQrHAlYtocRZWXBM6ruF3Bl7Lc7QgFHLRTfwMKo33KczlkvkXDY+qcytKhAWOT9dwUUYLUxAw==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.22.0" + } + }, "node_modules/@astrojs/prism": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", - "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.1.1.tgz", + "integrity": "sha512-sXlAXJX6t4QfuLg+CuCDzpg7NoxWcyb4paFprgtFppLeisCBmh935h+tJ5CWXfsXEoW2IDy/ihbRMtiu28Chjw==", "license": "MIT", "dependencies": { "prismjs": "^1.30.0" }, "engines": { - "node": ">=22.12.0" + "node": ">=22.23.4" } }, "node_modules/@astrojs/sitemap": { @@ -338,9 +364,9 @@ } }, "node_modules/@astrojs/starlight": { - "version": "0.42.2", - "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.2.tgz", - "integrity": "sha512-rDAgv3QCJOo0F4EoWeuFG31OdhQOrcipATl9+gCkOqpQiO7u7FR8uVam+HHNJO8Sl21TOAQKUSUEvCXv8jEgJA==", + "version": "0.42.6", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.6.tgz", + "integrity": "sha512-Cfs7GdgzgNRCauDxQVsFUVL1thrGHK/rQdbv/SURv/xhGBXx+j5ypOHW9ENPraQxbIzUC8KAnJV7L9mvEVRuyQ==", "license": "MIT", "dependencies": { "@astrojs/markdown-satteri": "^0.4.0", @@ -383,18 +409,18 @@ } }, "node_modules/@astrojs/telemetry": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.3.tgz", - "integrity": "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.4.0.tgz", + "integrity": "sha512-Qt2T6RGtYacyV8OZU3IcyFbsp69s6qIcnWvQvn7/p9MKebPCpEswP5Ji4RhifG3DfeBX7e07Pjj5smkd+Tfp7Q==", "license": "MIT", "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", - "package-manager-detector": "^1.6.0" + "package-manager-detector": "^1.9.0" }, "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0" + "node": ">=22.22.0" } }, "node_modules/@babel/helper-string-parser": { @@ -614,10 +640,41 @@ "node": ">=18" } }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-13.2.0.tgz", + "integrity": "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "13.2.0", + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/gast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-13.2.0.tgz", + "integrity": "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-13.2.0.tgz", + "integrity": "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw==", + "license": "Apache-2.0" + }, "node_modules/@chevrotain/types": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", - "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-13.2.0.tgz", + "integrity": "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==", "license": "Apache-2.0" }, "node_modules/@clack/core": { @@ -635,481 +692,69 @@ }, "node_modules/@clack/prompts": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", - "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", - "license": "MIT", - "dependencies": { - "@clack/core": "1.5.1", - "fast-string-width": "^3.0.2", - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } - }, - "node_modules/@ctrl/tinycolor": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz", - "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "dependencies": { + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, "engines": { - "node": ">=18" + "node": ">= 20.12.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], + "node_modules/@colordx/core": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@colordx/core/-/core-7.3.0.tgz", + "integrity": "sha512-LVzTTPhX7wpy7TkGjouA9zbbCBIw92RGm7ZQqSKFE0oYE3sMx2FGXz90eVpgw7k/+wrrY+qJ86nHXifZWMS5jA==", + "license": "MIT" + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz", + "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], "engines": { - "node": ">=18" + "node": ">=14" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@expressive-code/core": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.2.tgz", - "integrity": "sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==", + "version": "0.44.3", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.3.tgz", + "integrity": "sha512-pffktF0TJoxC08dgNcutl/0LcSnTrjF6vXR39qVpwZ0EyxpwO9zjI7dbni6sO7w3yHpRBO0voLFvhzB+EPJRsA==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.0.4", @@ -1118,46 +763,46 @@ "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", - "postcss-nested": "^6.0.1", + "postcss-nested": "^7.0.2", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "node_modules/@expressive-code/plugin-frames": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.2.tgz", - "integrity": "sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==", + "version": "0.44.3", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.3.tgz", + "integrity": "sha512-ysW4VXk9vXEliA99Ur6ApUepVtx74sPolp6o/j45BRjZQoL5O6kXKMhSP4koQLih8HbC5B3ImIvO+HKKhkv/oA==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.44.2" + "@expressive-code/core": "^0.44.3" } }, "node_modules/@expressive-code/plugin-line-numbers": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-line-numbers/-/plugin-line-numbers-0.44.2.tgz", - "integrity": "sha512-WvJV+Wv+dazle+IQoKH8rL5GpzwvRTSJ2IMrtufvnJy+aT+GGm73PoM19CN5HauCIc0tl/fGDtrm3iI9SjZNGQ==", + "version": "0.44.3", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-line-numbers/-/plugin-line-numbers-0.44.3.tgz", + "integrity": "sha512-cmaYH/ciJsC9MqNRuqWPjdjMAX9F9TxlJPqRN42tSYQ0Is15xBuaAYS88n1UPrlenk902PCYVHB152jfd6dxTA==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.44.2" + "@expressive-code/core": "^0.44.3" } }, "node_modules/@expressive-code/plugin-shiki": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.2.tgz", - "integrity": "sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==", + "version": "0.44.3", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.3.tgz", + "integrity": "sha512-jAibAZPG7lDmi61EFJADZjoSZzjHiwZZdbbgR/mTlWXM4ON/Pyo/9tB2vrqQNKylsci6jqtCi6idrwqQXF53xA==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.44.2", + "@expressive-code/core": "^0.44.3", "shiki": "^4.0.2" } }, "node_modules/@expressive-code/plugin-text-markers": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.2.tgz", - "integrity": "sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==", + "version": "0.44.3", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.3.tgz", + "integrity": "sha512-4Ae2xRSDV5kdWPsi40xWbOtoihbzM0zLVjQKkDAnD5KzFXl+6/kd+VhuWOlTRVaJ5cqGk++vCbAaf0MR1XbZlw==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.44.2" + "@expressive-code/core": "^0.44.3" } }, "node_modules/@iconify-json/mdi": { @@ -1196,9 +841,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -1214,13 +859,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -1236,20 +881,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -1259,9 +904,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -1275,9 +920,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -1291,9 +936,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -1307,9 +952,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -1323,9 +968,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -1339,9 +984,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -1355,9 +1000,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -1371,9 +1016,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -1387,9 +1032,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -1403,9 +1048,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -1419,9 +1064,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -1437,13 +1082,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -1459,13 +1104,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -1481,13 +1126,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -1503,13 +1148,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -1525,13 +1170,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -1547,13 +1192,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -1569,13 +1214,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -1591,13 +1236,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -1611,16 +1256,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -1630,9 +1275,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -1649,9 +1294,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -1668,9 +1313,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1777,19 +1422,31 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@mdx-js/mdx/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/@mermaid-js/parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", - "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-2.0.1.tgz", + "integrity": "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==", "license": "MIT", "dependencies": { - "@chevrotain/types": "~11.1.2" + "@chevrotain/types": "~13.2.0" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", - "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", + "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", "license": "MIT", "optional": true, "dependencies": { @@ -1814,9 +1471,9 @@ "license": "MIT" }, "node_modules/@oxc-parser/binding-android-arm-eabi": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.150.0.tgz", - "integrity": "sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.153.0.tgz", + "integrity": "sha512-BcWu25dMwMjHpaDCEidmpAi7rGg1qZoihJeUUH9bD55pVZ0z4Wa5CQgqSTo0R41H81oozisQKW3WqvAN2m77IQ==", "cpu": [ "arm" ], @@ -1830,9 +1487,9 @@ } }, "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.150.0.tgz", - "integrity": "sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.153.0.tgz", + "integrity": "sha512-9l5rdp2DhRqVriBHsNpu5cEf+hizZ4B58rt93HzyQEBUgZiC6qe2oFpaAAnXGCxHGnQExI1s7S2G0QhFacUZ6w==", "cpu": [ "arm64" ], @@ -1846,9 +1503,9 @@ } }, "node_modules/@oxc-parser/binding-darwin-arm64": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.150.0.tgz", - "integrity": "sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.153.0.tgz", + "integrity": "sha512-EcpsyRXHhENDpekpG4QNaSZj2F/BAz4Gh4o89zZwNss86R8KyRSQ6sXbAj74Xyeb4z3j/yBZZ6joQZhraIWyfw==", "cpu": [ "arm64" ], @@ -1862,9 +1519,9 @@ } }, "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.150.0.tgz", - "integrity": "sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.153.0.tgz", + "integrity": "sha512-hlj80HUQy07/2lYp6rmxH+3u+H2ik3rjLR52CUdPApxmZCNxASDUf/+ykTzAHS/fehftmVMATKvPaWmuLHuasg==", "cpu": [ "x64" ], @@ -1878,9 +1535,9 @@ } }, "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.150.0.tgz", - "integrity": "sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.153.0.tgz", + "integrity": "sha512-fWRcv2pMSh1qtez46MOu8vuUJ5fiaQZPHSphbWulbL7SHBMKfRgZl2PzjXZYQJbq7NEXyXbgIuIuUm+/Rmkv8w==", "cpu": [ "x64" ], @@ -1894,9 +1551,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.150.0.tgz", - "integrity": "sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.153.0.tgz", + "integrity": "sha512-paZeUK1GXShl/bOJQET1/8AQxayFeuN4SIWAkgTRxE7ZQwv9uYSB2n0648smygP22N7jcI3zxAm0Z+ALNiIKxg==", "cpu": [ "arm" ], @@ -1910,9 +1567,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.150.0.tgz", - "integrity": "sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.153.0.tgz", + "integrity": "sha512-qFPp5NwMf4K1C0O+DCVGmcbOaYyYAq98Rvv9p4VSfb777iUwlJCqMUxf6QG9N5f5ls4TEX6LitOZHi+6GL6rYQ==", "cpu": [ "arm" ], @@ -1926,9 +1583,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.150.0.tgz", - "integrity": "sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.153.0.tgz", + "integrity": "sha512-oufEhZCX7saeo27Logra8cvBR1jQ/yw2zUI2hHmWw3qA/XYqEW31FOvLQBv7DWN32NjVdlcaP11WBBZTJo4k9g==", "cpu": [ "arm64" ], @@ -1942,9 +1599,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.150.0.tgz", - "integrity": "sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.153.0.tgz", + "integrity": "sha512-zXYzuAqng85h6+LI3iRm0zeMLX1/K2z41ZAMWxVRgt0D10VvZUaV5q+TEB46KDhoKBpdexoTLC6QoB/EiP2oqQ==", "cpu": [ "arm64" ], @@ -1958,9 +1615,9 @@ } }, "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.150.0.tgz", - "integrity": "sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.153.0.tgz", + "integrity": "sha512-J7+NLwLEzdkKM9cKyPHH/AjXXstWOs98KYgLrPPnO4N9MO8EZRBfPNnBcS3S67uMOBkRpUmCl2FhRnWdQ5CbBA==", "cpu": [ "ppc64" ], @@ -1974,9 +1631,9 @@ } }, "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.150.0.tgz", - "integrity": "sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.153.0.tgz", + "integrity": "sha512-sD0K11GQTLWylln54nPMMTtgmBbQrR5It/vMpZpy/fogiw8O2AvIE+9LuWUXddrWk9zvFIHGRcSgFn4IHOfoYg==", "cpu": [ "riscv64" ], @@ -1990,9 +1647,9 @@ } }, "node_modules/@oxc-parser/binding-linux-riscv64-musl": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.150.0.tgz", - "integrity": "sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.153.0.tgz", + "integrity": "sha512-7L66uql2BhvSz51LwfExjOdn6et/ydTiQxl23VApdKzJhTc3TczA1Qt6VR/UcvlQrL2ZbytIJb447vh7p5wmUw==", "cpu": [ "riscv64" ], @@ -2006,9 +1663,9 @@ } }, "node_modules/@oxc-parser/binding-linux-s390x-gnu": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.150.0.tgz", - "integrity": "sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.153.0.tgz", + "integrity": "sha512-hXV3T7mNyrPR19rbPYkglkQJtoM/UNo/xwbZeeqdAxEg8PXgb4LXXuxPpGgI4bzRRCWwLUxdDSS58xIyC2u1TA==", "cpu": [ "s390x" ], @@ -2022,9 +1679,9 @@ } }, "node_modules/@oxc-parser/binding-linux-x64-gnu": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.150.0.tgz", - "integrity": "sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.153.0.tgz", + "integrity": "sha512-Da8Vqs1xdtoDYtEjmozD7wGgJeLBQkMbR0WUBECOSxodyX5DXy8ySlvJWbukyO2Q0+TM9EK+4LT6tbq0jCg4hQ==", "cpu": [ "x64" ], @@ -2038,9 +1695,9 @@ } }, "node_modules/@oxc-parser/binding-linux-x64-musl": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.150.0.tgz", - "integrity": "sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.153.0.tgz", + "integrity": "sha512-zGAPxNTIIw6X5WyTRscIFE/oDjpdNtrrl32ing6NOXdGjzRucadnxBilexLdeYQ9YxixF93GujpXyQ0jw7ddSg==", "cpu": [ "x64" ], @@ -2054,9 +1711,9 @@ } }, "node_modules/@oxc-parser/binding-openharmony-arm64": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.150.0.tgz", - "integrity": "sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.153.0.tgz", + "integrity": "sha512-GoHzpqP5XyPIp9DHokoEdCp+K4UPl/h1neFj1uXnUB3pJYyOQ7NOhFVOttNUgCGcj2eywAvu5cRivTOSq4S10g==", "cpu": [ "arm64" ], @@ -2070,9 +1727,9 @@ } }, "node_modules/@oxc-parser/binding-win32-arm64-msvc": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.150.0.tgz", - "integrity": "sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.153.0.tgz", + "integrity": "sha512-xGcy12tO9k7zFbeyQzEuilfWfQpyRq23/fLPfN3wjSWuWv58k46fQT1hsO1KFNykZuVGW9xSJE/z592TmAoSDA==", "cpu": [ "arm64" ], @@ -2086,9 +1743,9 @@ } }, "node_modules/@oxc-parser/binding-win32-ia32-msvc": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.150.0.tgz", - "integrity": "sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.153.0.tgz", + "integrity": "sha512-o1ykUjWncvXX6N5a4KebgLjtD8zRui+f1llMhSGoSC9oP2KOqapRpdwmZ3NcNG0axFouwVKmhGBjd+IhA8BksA==", "cpu": [ "ia32" ], @@ -2102,9 +1759,9 @@ } }, "node_modules/@oxc-parser/binding-win32-x64-msvc": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.150.0.tgz", - "integrity": "sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.153.0.tgz", + "integrity": "sha512-gxQz7G96Up136fd+2zWrP+o6FYGs9NO4Dyv55IoeJFJUDwLFHcLYY11hhA5wQKs0HoM57Q8esQ3nEVhnCdsjAQ==", "cpu": [ "x64" ], @@ -2118,9 +1775,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", - "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/oxc-project" @@ -2236,9 +1893,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", - "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", "cpu": [ "arm" ], @@ -2252,9 +1909,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", - "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", "cpu": [ "arm64" ], @@ -2268,9 +1925,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", - "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", "cpu": [ "arm64" ], @@ -2284,9 +1941,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", - "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", "cpu": [ "x64" ], @@ -2300,9 +1957,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", - "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", "cpu": [ "x64" ], @@ -2316,9 +1973,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", - "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", "cpu": [ "arm" ], @@ -2332,9 +1989,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", - "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", "cpu": [ "arm64" ], @@ -2348,9 +2005,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", - "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", "cpu": [ "arm64" ], @@ -2364,9 +2021,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", - "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", "cpu": [ "ppc64" ], @@ -2380,9 +2037,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", - "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", "cpu": [ "s390x" ], @@ -2396,9 +2053,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", - "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", "cpu": [ "x64" ], @@ -2412,9 +2069,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", - "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", "cpu": [ "x64" ], @@ -2428,9 +2085,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", - "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", "cpu": [ "arm64" ], @@ -2444,9 +2101,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", - "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", "cpu": [ "arm64" ], @@ -2460,9 +2117,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", - "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", "cpu": [ "x64" ], @@ -2482,13 +2139,13 @@ "license": "MIT" }, "node_modules/@shikijs/core": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", - "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz", + "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==", "license": "MIT", "dependencies": { - "@shikijs/primitive": "4.4.3", - "@shikijs/types": "4.4.3", + "@shikijs/primitive": "4.5.0", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" @@ -2498,12 +2155,12 @@ } }, "node_modules/@shikijs/engine-javascript": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", - "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz", + "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.4.3", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" }, @@ -2512,12 +2169,12 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", - "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz", + "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.4.3", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2" }, "engines": { @@ -2525,24 +2182,24 @@ } }, "node_modules/@shikijs/langs": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", - "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz", + "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.4.3" + "@shikijs/types": "4.5.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/primitive": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", - "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz", + "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.4.3", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" }, @@ -2551,21 +2208,21 @@ } }, "node_modules/@shikijs/themes": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", - "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz", + "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.4.3" + "@shikijs/types": "4.5.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/types": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", - "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -2576,9 +2233,9 @@ } }, "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.1.0.tgz", + "integrity": "sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==", "license": "MIT" }, "node_modules/@tybys/wasm-util": { @@ -2835,9 +2492,9 @@ } }, "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", + "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", "license": "MIT", "dependencies": { "@types/d3-interpolate": "*", @@ -2920,12 +2577,12 @@ } }, "node_modules/@types/node": { - "version": "24.13.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz", - "integrity": "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==", + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/picomatch": { @@ -2963,14 +2620,14 @@ "license": "ISC" }, "node_modules/@unocss/astro": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/astro/-/astro-66.10.5.tgz", - "integrity": "sha512-9MAIm6ufLtiiSFwfCLqwSwKiogYckTIYzVW6urrA0Ok9ZOwpwNW+omH8HljxYUxU94K092dBexsLFF5VstgjDA==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/astro/-/astro-66.11.0.tgz", + "integrity": "sha512-UaDsy5Tf+H87TQr6tu6E2+K3t6YO4dEIMCqxQwWRV1n5qvx0Tj/jD8K+/u10D5cKDMmG4ne9J6yB8C621Q5lTA==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "@unocss/reset": "66.10.5", - "@unocss/vite": "66.10.5" + "@unocss/core": "66.11.0", + "@unocss/reset": "66.11.0", + "@unocss/vite": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -2984,23 +2641,36 @@ } } }, + "node_modules/@unocss/autocomplete": { + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/autocomplete/-/autocomplete-66.11.0.tgz", + "integrity": "sha512-+lxwoW2J8iLfBJkjb1eEzamhWAtrsEEUaiGbdOyl5IQN8CekigUfFJ8UVlJklCVaRyiK1RbIvVWK+SC/kBUkvg==", + "license": "MIT", + "dependencies": { + "fzf": "^0.5.2", + "lru-cache": "^11.5.3" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@unocss/cli": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/cli/-/cli-66.10.5.tgz", - "integrity": "sha512-NjHkCNRsgnrU7m/hI1y2ryv8RMku+uejx9ChCnKcdKVKuBpgKFdhjmSjaTJQjpAHORyv5hLdT/7TkLbJzqk4tg==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/cli/-/cli-66.11.0.tgz", + "integrity": "sha512-OUviuT/PTYIUxbkD7VUhbQgEtEFz4OBfWLAQNP6j0bN6B2YPxY+ejshVWHRP0n8flZfWtUOPJR/Q+inKkKiZaA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "@unocss/config": "66.10.5", - "@unocss/core": "66.10.5", - "@unocss/preset-wind3": "66.10.5", - "@unocss/preset-wind4": "66.10.5", - "@unocss/transformer-directives": "66.10.5", - "cac": "^7.0.0", + "@unocss/config": "66.11.0", + "@unocss/core": "66.11.0", + "@unocss/preset-wind3": "66.11.0", + "@unocss/preset-wind4": "66.11.0", + "@unocss/transformer-directives": "66.11.0", + "cac": "^7.1.0", "chokidar": "^5.0.0", "colorette": "^2.0.20", "consola": "^3.4.2", - "magic-string": "^1.4.1", + "magic-string": "^1.4.3", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "tinyglobby": "^0.2.17", @@ -3014,12 +2684,12 @@ } }, "node_modules/@unocss/config": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/config/-/config-66.10.5.tgz", - "integrity": "sha512-ddmmmsJ+Vjg9oS4c5ElP8vR1oftID9dqG0yASDWLsLNY4uKo295EuCQXIBVbkyRnBZcdjpnOUfo71iwElaCeyA==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/config/-/config-66.11.0.tgz", + "integrity": "sha512-ohpkpfN4GAY7itfDJmeKH+KFbAUoKOVRa3ChJLvUgMPFcpnwjxhDB0ePREqTe60K6kL6AblrcfSW2Vs46z9bGQ==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", + "@unocss/core": "66.11.0", "colorette": "^2.0.20", "consola": "^3.4.2", "unconfig": "^7.5.0" @@ -3029,36 +2699,38 @@ } }, "node_modules/@unocss/core": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/core/-/core-66.10.5.tgz", - "integrity": "sha512-fukv4bL32z/kWIiKgDy5ih452Jzf7S76p8RnP4N+3utqMFeqWayEApiqRidGv5b6NwPguTsKXaLsy+mbK9aMfA==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/core/-/core-66.11.0.tgz", + "integrity": "sha512-eDMgNWF0LnpNQJXiBEofsAbh2EusQjkVDefBC526U5vOqRwMmqNuYs32rvUwYxf4O/ryIEgFJQAvDv+JpwfSKg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/extractor-arbitrary-variants": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-66.10.5.tgz", - "integrity": "sha512-8qCJjByALdNiGQB1xHWK3tAl3n+hacBLlklqHhgNj0qWYtr3Hp+pfyA2nuyzJcPXkolqtGn1APJ1TNPbL5w+qg==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-66.11.0.tgz", + "integrity": "sha512-lYr95OSbeE+3q4P3ACkBA1PBSFT82k827DTo31/2OaMryzE2s9NQVdHRZ2XslHDmyC8eeuddx8I1Cq+cN960Pw==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5" + "@unocss/core": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/inspector": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/inspector/-/inspector-66.10.5.tgz", - "integrity": "sha512-YTYkwzBrEnfleoIv9Q7PQDlbC7st/hUKI8V7veWwkqpzcAxGcLfbLn50jGgnbvAZrxtGgvF021uW6aor9FyX6A==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/inspector/-/inspector-66.11.0.tgz", + "integrity": "sha512-1l+2DzlOEUp1uwI/IQK+YYoqUNOo2aq1opaOExyQlsEphSsIVZeZ9MWf+RUwtUACkkm05729aubvkpV0VydyNw==", "license": "MIT", "dependencies": { - "@unocss/config": "66.10.5", - "@unocss/core": "66.10.5", - "@unocss/rule-utils": "66.10.5", - "devframe": "^1.0.0", + "@unocss/autocomplete": "66.11.0", + "@unocss/config": "66.11.0", + "@unocss/core": "66.11.0", + "@unocss/language-server": "66.11.0", + "@unocss/rule-utils": "66.11.0", + "devframe": "^1.2.3", "tinyglobby": "^0.2.17", "unplugin-utils": "^0.3.2" }, @@ -3066,26 +2738,49 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@unocss/language-server": { + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/language-server/-/language-server-66.11.0.tgz", + "integrity": "sha512-h7P1f6QID4qAiXiuXGCtcCi+jnlXg1TaJAMpc5ISIBI59H/XtmMBg47TpsY+u/1vXREcNKsKYIR/MUO0E5Y92g==", + "license": "MIT", + "dependencies": { + "@colordx/core": "^7.3.0", + "@unocss/autocomplete": "66.11.0", + "@unocss/config": "66.11.0", + "@unocss/core": "66.11.0", + "@unocss/preset-wind3": "66.11.0", + "prettier": "^3.9.10", + "unconfig": "^7.5.0", + "vscode-languageserver": "^10.1.2", + "vscode-languageserver-textdocument": "^1.0.15" + }, + "bin": { + "unocss-language-server": "bin/unocss-language-server.js" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@unocss/preset-attributify": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-attributify/-/preset-attributify-66.10.5.tgz", - "integrity": "sha512-2wgfTLGe7ryG9FwAMhNNpfzuL7jbBRWP4VgeGLnKusGIPrzmJ+bF3/+EZTgVZwAi+5jhj6wofcEJLvmHRUOYMw==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-attributify/-/preset-attributify-66.11.0.tgz", + "integrity": "sha512-A6oeH14qMdgQCxPZmIFYgZfSEC555K8Izvc3U6iLl+3k3qsIgRsiqxcS8U+o6Qwk8QXu0E+phUpzHLddTa2jUA==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5" + "@unocss/core": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/preset-icons": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-icons/-/preset-icons-66.10.5.tgz", - "integrity": "sha512-YZuDehWpwpvLzkMPxO329fhd66GKpblhbUHrL/5uVMAXJpxK7ZVSQECgpIbU8sXlc4/gvTOjkIFD0fZ1MRPK7A==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-icons/-/preset-icons-66.11.0.tgz", + "integrity": "sha512-MldI3Bi6IgQWaFJ8MtMOEqXx/sveuf+kko8yNEgpa/PpNj1lHsndcJTWP+VGK7PxnHsj/3cqzmdoh2qJhkgs6Q==", "license": "MIT", "dependencies": { "@iconify/utils": "^3.1.7", - "@unocss/core": "66.10.5", + "@unocss/core": "66.11.0", "ofetch": "^1.5.1" }, "funding": { @@ -3093,61 +2788,61 @@ } }, "node_modules/@unocss/preset-mini": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-mini/-/preset-mini-66.10.5.tgz", - "integrity": "sha512-oFKjmK32jyKp8rMI8Zu5+1AaFOvbwlQ7wztNGjtiAhhglKREZoncThLBE6Yd3B2+p/xf8YXfSOr5TFzhzXnDIQ==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-mini/-/preset-mini-66.11.0.tgz", + "integrity": "sha512-MtzRKtK07OLhMph2dj+YFIhIxjS5WQ9N0I8btBJ7+rsrN/IISo538S8s4Pf3usCSg3ALCN4KX/GRBRx47PS2pQ==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "@unocss/extractor-arbitrary-variants": "66.10.5", - "@unocss/rule-utils": "66.10.5" + "@unocss/core": "66.11.0", + "@unocss/extractor-arbitrary-variants": "66.11.0", + "@unocss/rule-utils": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/preset-tagify": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-tagify/-/preset-tagify-66.10.5.tgz", - "integrity": "sha512-8lefFZt4H4N+hu48vp+aqqQcTG2P28ox4tTRXgpTrhk4toYFrY2YreMqa3BFvOmZsBRunr/pdd7ZwDi5vt5nmg==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-tagify/-/preset-tagify-66.11.0.tgz", + "integrity": "sha512-6PcgSVd2eKblMZX/YjaFtpyS0dRX/ZUbWamTlgDRhnf6j2nfA5DQHU3BZJNeUr83oXullDxFflPwpfGrV1aOxA==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5" + "@unocss/core": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/preset-typography": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-typography/-/preset-typography-66.10.5.tgz", - "integrity": "sha512-b7hP1v4LyopVRrzJiI+ymnJEZMoLW1DdBo0SFWyPnFZXJv7umtDFlZ9plb+8AJvviyRgZ3qI03adRLXA4QCt2g==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-typography/-/preset-typography-66.11.0.tgz", + "integrity": "sha512-s/xbtXG1f0E0n288m2gtxN3GcNMoHPNZZk1Qud60MlZzAtdXVE4G01GRPnUjhUa2xlSHDReJV3ewufrQWyyh+w==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "@unocss/rule-utils": "66.10.5" + "@unocss/core": "66.11.0", + "@unocss/rule-utils": "66.11.0" } }, "node_modules/@unocss/preset-uno": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-uno/-/preset-uno-66.10.5.tgz", - "integrity": "sha512-Okd40CCXMWzv4fJQplNWCiARdPnAo5ghALvo2YQrYGaqwL4qwDurw4KK1R62PmvDCKXPPQY9duzcje/uOkvmeQ==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-uno/-/preset-uno-66.11.0.tgz", + "integrity": "sha512-7Mbelwsvg/qxPGZ9ePzLzn3qwyouck+BeM2SS90OR1MXIoFxobl80FRRCjg+XgCU8RkaBiyyTy1mUdqUjobp8Q==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "@unocss/preset-wind3": "66.10.5" + "@unocss/core": "66.11.0", + "@unocss/preset-wind3": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/preset-web-fonts": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-web-fonts/-/preset-web-fonts-66.10.5.tgz", - "integrity": "sha512-7yz51nOMorsHqKVpWLjp9aHX2bW6fgRkKPY/mZnpw/1S8rpYNOeZPv4w6ogaSIyCvQppDnxPnwp2hQwwnNKi2Q==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-web-fonts/-/preset-web-fonts-66.11.0.tgz", + "integrity": "sha512-x205P3DAMiEIp1bh3KNvo8Hs13fC7r8LJ1mq8YFzAPlOZ5ejiv7/8WkihOba4Zf07FPHUjFDXNX9VPpeIjdMIA==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", + "@unocss/core": "66.11.0", "ofetch": "^1.5.1" }, "funding": { @@ -3155,66 +2850,66 @@ } }, "node_modules/@unocss/preset-wind": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-wind/-/preset-wind-66.10.5.tgz", - "integrity": "sha512-K2vnt3ri39kbToG5x4j56kB9pTLLsJuPZfQbWcEAnwOq8ojW5g80dCxetW3Rqukou/u2NnAJmAKm93aG0RZFIQ==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind/-/preset-wind-66.11.0.tgz", + "integrity": "sha512-LBDVrr/YWrQ8vT0bSJlLCINFEg6loSUMlFFM2e8R45NJ5SGlxPz534nOrAbunsewu1bIcqXMZTvhKW6pXS/Rbw==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "@unocss/preset-wind3": "66.10.5" + "@unocss/core": "66.11.0", + "@unocss/preset-wind3": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/preset-wind3": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-wind3/-/preset-wind3-66.10.5.tgz", - "integrity": "sha512-1zjLAJ4JCVREQlRaLiajP5T/BQUQzc2ilpScD1kvgTkJLV8en+ae1G/ctjZwAThmosD0oicuuMT9ODjt2NfGOw==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind3/-/preset-wind3-66.11.0.tgz", + "integrity": "sha512-TD0nZjOfPmkLuR0Ax9SEBzCE8F5oKCMBWRqgEcfoSBPRDFfuyM/2BQEsdlbNRJ5JPe97fYFe8tmS64VNXZmgSg==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "@unocss/preset-mini": "66.10.5", - "@unocss/rule-utils": "66.10.5" + "@unocss/core": "66.11.0", + "@unocss/preset-mini": "66.11.0", + "@unocss/rule-utils": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/preset-wind4": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/preset-wind4/-/preset-wind4-66.10.5.tgz", - "integrity": "sha512-rYLe20SyOEf5V9SsHkHVhdmFGE4qZJL0/yheZGAUXXv/kN0yR+qlMPB4Q6UR4JiDxrR5R+8U7BIa/juOks7EvA==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind4/-/preset-wind4-66.11.0.tgz", + "integrity": "sha512-ETXP3l0z8ceI1xPEhNld0ihYAH6y6tKmHlG/D37MmWSA1eQoSZoNriWed/eo9leQVkV5sP7j85DXBG/oAffbQg==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "@unocss/extractor-arbitrary-variants": "66.10.5", - "@unocss/rule-utils": "66.10.5" + "@unocss/core": "66.11.0", + "@unocss/extractor-arbitrary-variants": "66.11.0", + "@unocss/rule-utils": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/reset": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/reset/-/reset-66.10.5.tgz", - "integrity": "sha512-AElCFeXaK+3a8LX1obMSmhYhhlRKDXXVngn0Kse7XwxHOItQqpncTU6ZKUeNNGmeoE0Vwc6MfrLw6eGE7EEpgw==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/reset/-/reset-66.11.0.tgz", + "integrity": "sha512-QyXwD3bOzi/zZBW8jte9ierrx5+iPXQjPUdiSqyAaJd+RxajZQV9m9lKiadWwn8pV082mc2b4y4Acvi5lLTTWg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/rollup": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/rollup/-/rollup-66.10.5.tgz", - "integrity": "sha512-Ii1srTmJ1kF3yHdt1vdIiYID4cAeaxooSQJ1LIWLxy0Xr2/QVXKlcxp9KdXti8nfrmPcx3oWYDa92sEHVxIpgQ==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/rollup/-/rollup-66.11.0.tgz", + "integrity": "sha512-8/EkVkJUHTH27f6oNtMT22FPSl+edNpFGTYYgpF9N+CCUcMKCGTuifF0Svb72ILOpTCKhq+PP3/HPMfb5faI3A==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "@unocss/config": "66.10.5", - "@unocss/core": "66.10.5", + "@unocss/config": "66.11.0", + "@unocss/core": "66.11.0", "chokidar": "^5.0.0", - "magic-string": "^1.4.1", + "magic-string": "^1.4.3", "pathe": "^2.0.3", "tinyglobby": "^0.2.17", "unplugin-utils": "^0.3.2" @@ -3236,79 +2931,79 @@ } }, "node_modules/@unocss/rule-utils": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/rule-utils/-/rule-utils-66.10.5.tgz", - "integrity": "sha512-ePcz9fJfEVN4VXJgx2bPdGSZ/ujdyUhiJOnMTwIWRxlUdZ2Wgc5aT6zTb9W9AjOB0eWxO2Iyb0HGhA9GGuIY8g==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/rule-utils/-/rule-utils-66.11.0.tgz", + "integrity": "sha512-QW7B4whiR9x6m7FHEdsDuObFY9lbuoZunVoTvmZZDNSN/GNmdDNzMNmeCSLq1+QeEvz1ac5LnxRXdQzcrQ2MVg==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "magic-string": "^1.4.1" + "@unocss/core": "66.11.0", + "magic-string": "^1.4.3" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/transformer-attributify-jsx": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-66.10.5.tgz", - "integrity": "sha512-G3QPttsY2ZLgLPTliNwQTPrESNXTr0B9NiTy/qHcRcUBfjunB4Et2wcBG4JAiaZCdLikYCuR+g9O+GveCbboAw==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-66.11.0.tgz", + "integrity": "sha512-V0U6qSuKIilm2IzozdY2ipMgToFJ1SGjCbBdoEIthwzfZ0VvuO7pSy/EPtkW5MiFFISHBA9UMfqSvR3IH1YHyA==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "oxc-parser": "^0.150.0", - "oxc-walker": "^0.7.0" + "@unocss/core": "66.11.0", + "oxc-parser": "^0.153.0", + "oxc-walker": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/transformer-compile-class": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/transformer-compile-class/-/transformer-compile-class-66.10.5.tgz", - "integrity": "sha512-JHTSocPg3boTxkFvCVvVwoGlgyyoJ9YhZBy+c2hxW44mCIOGFimsRvslRg7RscYRR2ts8cdQhC3Oi4Wce+BHQA==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/transformer-compile-class/-/transformer-compile-class-66.11.0.tgz", + "integrity": "sha512-/o7x70nSHZC210fZ900PvVowLSg0dPk55WBFMzpiXVm5o/SBWJT/fAaSvmdjKz1xEoLbqjJzgbHtTfY4PB99+A==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5" + "@unocss/core": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/transformer-directives": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/transformer-directives/-/transformer-directives-66.10.5.tgz", - "integrity": "sha512-X7WkyesHojKlDBPzgytRzqgftI7lRqpSbSdz7kx+d09D/Xf/1V7i0IOc+mBxeFngvqh2ueuLElwZbaT3CuKHkg==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/transformer-directives/-/transformer-directives-66.11.0.tgz", + "integrity": "sha512-6D6NOA67nk7FH1QsUE3Xb/93Mocwbgqos6LoxdmeO8c3D1Z/fNaJR3rH6z8KH0QXemNciGO0HUCPSjsOR0x1Dg==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5", - "@unocss/rule-utils": "66.10.5", + "@unocss/core": "66.11.0", + "@unocss/rule-utils": "66.11.0", "css-tree": "^3.2.1" } }, "node_modules/@unocss/transformer-variant-group": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/transformer-variant-group/-/transformer-variant-group-66.10.5.tgz", - "integrity": "sha512-e1l3bCxl4qMizPJPe6E6tfS2swdL9r/ohiKxSxku6+/kyKYnAxRcpVPK6IbmkurQfo73cjSJUV+zXmqdxbdWNQ==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/transformer-variant-group/-/transformer-variant-group-66.11.0.tgz", + "integrity": "sha512-4djiS1UPG6GfA5MDjGurLnYaUHLAaKWl1N7BbDgqXrBWnnKTHobB7fe3MJX94DT7AzmLFjrW5SddeGm4vEh4gw==", "license": "MIT", "dependencies": { - "@unocss/core": "66.10.5" + "@unocss/core": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@unocss/vite": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/@unocss/vite/-/vite-66.10.5.tgz", - "integrity": "sha512-aFJ+lTcqIbtqxsKD1OI1gtvfEieJPVa9KOZr1Wj/ChrSCStcS7QP/DTbYvM6hC+qa07W6hEbSEgYqOUtCvFKVw==", + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/@unocss/vite/-/vite-66.11.0.tgz", + "integrity": "sha512-1kfKcuF8bAAf+ZDkvCYsDhAfAz+vM6d5SjntabPSrsv7O1h7++r93Nw/gOE30nrITKZ1UynLE0yXO0GQ2zmrkg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "@unocss/config": "66.10.5", - "@unocss/core": "66.10.5", - "@unocss/inspector": "66.10.5", + "@unocss/config": "66.11.0", + "@unocss/core": "66.11.0", + "@unocss/inspector": "66.11.0", "chokidar": "^5.0.0", - "magic-string": "^1.4.1", + "magic-string": "^1.4.3", "pathe": "^2.0.3", "tinyglobby": "^0.2.17", "unplugin-utils": "^0.3.2" @@ -3331,9 +3026,9 @@ } }, "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3352,9 +3047,9 @@ } }, "node_modules/am-i-vibing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz", - "integrity": "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.5.0.tgz", + "integrity": "sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==", "license": "MIT", "dependencies": { "process-ancestry": "^0.1.0" @@ -3444,70 +3139,69 @@ } }, "node_modules/astro": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/astro/-/astro-7.3.3.tgz", - "integrity": "sha512-NF08hk3edFkVmr9avf9HW/rQyf6NrpbVDyYj2cVN3JfijDhfJYh1ivWVTY5PUd5+rRZn/Vtu4iaeyNlP0Iv6mg==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.4.1.tgz", + "integrity": "sha512-mzWNEWSef1VQDgFku6Gp0v8Qx53NOkNlOIDQcX9ovxAZ1QXic6cks/FFo+gxPM9anXuat4X1FhB61KTk3XFvgg==", "license": "MIT", "dependencies": { - "@astrojs/compiler-rs": "^0.4.0", - "@astrojs/internal-helpers": "0.11.0", - "@astrojs/markdown-satteri": "0.4.1", - "@astrojs/telemetry": "3.3.3", - "@capsizecss/unpack": "^4.0.0", - "@clack/prompts": "^1.1.0", + "@astrojs/compiler-rs": "^0.5.1", + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/markdown-satteri": "0.4.4", + "@astrojs/telemetry": "3.4.0", + "@capsizecss/unpack": "^4.0.1", + "@clack/prompts": "^1.8.1", "@oslojs/encoding": "^1.1.0", - "am-i-vibing": "^0.4.0", + "am-i-vibing": "^0.5.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", - "devalue": "^5.8.1", + "devalue": "^6.0.2", "diff": "^9.0.0", "dset": "^3.1.4", - "es-module-lexer": "^2.0.0", - "esbuild": "^0.28.0", - "find-proc": "0.2.0", + "es-module-lexer": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.4.1", - "get-tsconfig": "5.0.0-beta.4", + "get-tsconfig": "5.0.0-beta.6", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", - "http-cache-semantics": "^4.2.0", - "js-yaml": "^4.3.0", + "http-cache-semantics": "^4.3.0", "jsonc-parser": "^3.3.1", - "magic-string": "^1.0.0", - "magicast": "^0.5.2", + "magic-string": "^1.4.3", + "magicast": "^0.5.5", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", - "obug": "^2.1.1", - "p-limit": "^7.3.0", - "p-queue": "^9.1.0", - "package-manager-detector": "^1.6.0", + "obug": "^3.0.0", + "p-limit": "^7.3.3", + "p-queue": "^9.3.5", + "package-manager-detector": "^1.9.0", "piccolore": "^0.1.3", - "picomatch": "^4.0.4", - "shiki": "^4.0.2", - "smol-toml": "^1.6.0", - "svgo": "^4.0.2", - "tinyclip": "^0.1.12", - "tinyexec": "^1.0.4", - "tinyglobby": "^0.2.15", - "ultrahtml": "^1.6.0", - "unifont": "~0.7.5", + "picomatch": "^4.0.7", + "rolldown": "^1.2.13", + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "svgo": "^4.1.0", + "tinyclip": "^1.0.3", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "tinyps": "^0.5.0", + "ultrahtml": "^1.7.0", + "unifont": "^1.0.3", "unstorage": "^1.17.5", - "verkit": "^0.4.0", - "vite": "^8.0.13", - "vitefu": "^1.1.2", + "verkit": "^0.5.0", + "vite": "^8.3.4", + "vitefu": "^1.1.3", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", - "zod": "^4.5.4" + "zod": "^4.6.6" }, "bin": { "astro": "bin/astro.mjs" }, "engines": { - "node": ">=22.12.0", + "node": ">=22.22.0", "npm": ">=9.6.5", "pnpm": ">=7.1.0" }, @@ -3540,12 +3234,12 @@ } }, "node_modules/astro-expressive-code": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.2.tgz", - "integrity": "sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==", + "version": "0.44.3", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.3.tgz", + "integrity": "sha512-LUpQmmvzr90JXP5QIAopgdjlOKLuT3x0wlPAUpc4OZtPtFS8sbflPcGPpL/lG3pQuUCQV2CXUFgYNBYNu9tphA==", "license": "MIT", "dependencies": { - "rehype-expressive-code": "^0.44.2", + "rehype-expressive-code": "^0.44.3", "url-extras": "^0.1.0" }, "peerDependencies": { @@ -3573,6 +3267,30 @@ } } }, + "node_modules/astro/node_modules/@astrojs/markdown-satteri": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.4.tgz", + "integrity": "sha512-Byhe9CAiGFJrhMACKpgN/FPf4lcwapqwFQAB7E1sPELTEJINfBIpgnEYOBTXzr//fvSSNhfp/X3AqN4mUV9p+A==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/prism": "4.1.0", + "github-slugger": "^2.0.0", + "satteri": "^0.10.5" + } + }, + "node_modules/astro/node_modules/@astrojs/prism": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.1.0.tgz", + "integrity": "sha512-zvRaL3MKTAXWfVQrHAlYtocRZWXBM6ruF3Bl7Lc7QgFHLRTfwMKo33KczlkvkXDY+qcytKhAWOT9dwUUYLUxAw==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.22.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3602,9 +3320,9 @@ } }, "node_modules/bcp-47": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", - "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.2.tgz", + "integrity": "sha512-wsv86UuNFuPnVieKLyECTT+CP1WSAHeoIg5P3xu2kA9Z+7fVPoKt7dxzSeEhtJel0AI+NW3fIW3KtTKQKUCngg==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -3645,9 +3363,9 @@ } }, "node_modules/cac": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", - "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.1.0.tgz", + "integrity": "sha512-LMDU/U2yA4IPIY8KT0NsNPWEYZvAMkbgyspxJ6btJaYV+WMDOHAZCmB0lsTObiHaRS1V/ztKAl7+TSkTqXyeoA==", "license": "MIT", "engines": { "node": ">=20.19.0" @@ -3703,6 +3421,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chevrotain": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz", + "integrity": "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "13.2.0", + "@chevrotain/gast": "13.2.0", + "@chevrotain/regexp-to-ast": "13.2.0", + "@chevrotain/types": "13.2.0", + "@chevrotain/utils": "13.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -3769,12 +3503,12 @@ } }, "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", "engines": { - "node": ">=16" + "node": ">= 10" } }, "node_modules/common-ancestor-path": { @@ -3786,12 +3520,6 @@ "node": ">= 18" } }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" - }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -3844,12 +3572,17 @@ } }, "node_modules/crossws": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", - "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.4.15.tgz", + "integrity": "sha512-NsUOpDZosz7nyI+qEIl05yaiXOWaXH2oTeM+XBWtiSWZtb5rzcIsl1AST/HzM2jzUn76eFuyrkn83E6iSBRUvw==", "license": "MIT", - "dependencies": { - "uncrypto": "^0.1.3" + "peerDependencies": { + "srvx": ">=0.11.5" + }, + "peerDependenciesMeta": { + "srvx": { + "optional": true + } } }, "node_modules/css-select": { @@ -3955,9 +3688,9 @@ "license": "CC0-1.0" }, "node_modules/cytoscape": { - "version": "3.34.3", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", - "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", + "version": "3.34.4", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.4.tgz", + "integrity": "sha512-msY99tMY8iWTg+sIPGfSqDcPNOt4xcj+v/K/7qUFaCUxxlqnMkUYLBjRUzn9W5CU/R2hrmtJa5+UtovrEM2ctg==", "license": "MIT", "engines": { "node": ">=0.10" @@ -4172,15 +3905,6 @@ "node": ">=12" } }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -4538,19 +4262,22 @@ } }, "node_modules/devalue": { - "version": "5.9.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", - "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", - "license": "MIT" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-6.0.3.tgz", + "integrity": "sha512-3woMEITftG1wKeO/4d/QXH1DVKctTSZg3jNGPpjp9hNr5Hos8MCFF5McTML5ZtgKDObm2R591B0spRp2IVernA==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/devframe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devframe/-/devframe-1.1.0.tgz", - "integrity": "sha512-pvp42kOkiEr32Msqngb3mW9AQFHwOxNMn8MYNO/TBkJhyeykbDUWHwiZVubRYnasmZeqT4MLYGF9AHjHIkmDnA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/devframe/-/devframe-1.2.6.tgz", + "integrity": "sha512-mHzcm6ADzXgRMWdoSX+5PjwhmwAuC3SpDPi3+URznlpzxJF6tXwy5h1xr82zPKju38EVtdnYRnnP4hZwLK01vA==", "license": "MIT", "dependencies": { "crossws": "^0.4.12", - "h3": "^2.0.1-rc.32", + "h3": "^2.0.1", "nostics": "^1.3.0", "pathe": "^2.0.3" }, @@ -4558,7 +4285,7 @@ "devframe": "bin/devframe.mjs" }, "peerDependencies": { - "@devframes/agentic": "1.1.0", + "@devframes/agentic": "1.2.6", "cac": "^7.0.0" }, "peerDependenciesMeta": { @@ -4570,48 +4297,6 @@ } } }, - "node_modules/devframe/node_modules/crossws": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.4.12.tgz", - "integrity": "sha512-aypfsr6t0uNvkqaZc6zvBfXzC6pLI0/sIulpkV6RwCVtZqG5ebBzv4weImKK0VNCj91Wl9F5j7p5WU4MNrybng==", - "license": "MIT", - "peerDependencies": { - "srvx": ">=0.11.5" - }, - "peerDependenciesMeta": { - "srvx": { - "optional": true - } - } - }, - "node_modules/devframe/node_modules/h3": { - "version": "2.0.1-rc.32", - "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.1-rc.32.tgz", - "integrity": "sha512-Epg1E85L8yIKd9UeAQAVdm6QVrnHbppMsQ641EjpNi4c0IlAg+PJlwp4Xw03Z1loRXPQaH9k64gJT5qDELqm+g==", - "license": "MIT", - "dependencies": { - "rou3": "^0.9.2", - "srvx": "^1.0.5" - }, - "bin": { - "h3": "bin/h3.mjs" - }, - "engines": { - "node": ">=20.11.1" - }, - "peerDependencies": { - "crossws": "^0.4.12", - "ocache": ">=0.3.0" - }, - "peerDependenciesMeta": { - "crossws": { - "optional": true - }, - "ocache": { - "optional": true - } - } - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -4661,6 +4346,18 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -4689,9 +4386,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", - "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -4720,10 +4417,16 @@ "node": ">=4" } }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" + }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -4745,9 +4448,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", "license": "MIT" }, "node_modules/es-toolkit": { @@ -4784,55 +4487,14 @@ "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", "license": "MIT", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "acorn": "^8.0.0", - "esast-util-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/escape-string-regexp": { @@ -4915,6 +4577,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-util-to-js/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/estree-util-visit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", @@ -4945,15 +4616,15 @@ "license": "MIT" }, "node_modules/expressive-code": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.2.tgz", - "integrity": "sha512-MLNghkvhyFgVW4oCD5DxBuYnnTXIf0PygzGmfT6OZeOOYWh/D4sH1j931biElkA5RG/J9wY/iuSMGAPF5vzHOg==", + "version": "0.44.3", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.3.tgz", + "integrity": "sha512-7RGslhJTNt01pdn5I3QmVh5C8ZIiSm0A+ORQ9j9r39oOgJVKjn/7dXl9rMT/SqmFLE6pJkTYI7UjW0BxJnW7OQ==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.44.2", - "@expressive-code/plugin-frames": "^0.44.2", - "@expressive-code/plugin-shiki": "^0.44.2", - "@expressive-code/plugin-text-markers": "^0.44.2" + "@expressive-code/core": "^0.44.3", + "@expressive-code/plugin-frames": "^0.44.3", + "@expressive-code/plugin-shiki": "^0.44.3", + "@expressive-code/plugin-text-markers": "^0.44.3" } }, "node_modules/extend": { @@ -4986,15 +4657,6 @@ "fast-string-width": "^3.0.2" } }, - "node_modules/fastdom": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastdom/-/fastdom-1.0.12.tgz", - "integrity": "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg==", - "license": "MIT", - "dependencies": { - "strictdom": "^1.0.1" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5012,15 +4674,6 @@ } } }, - "node_modules/find-proc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.2.0.tgz", - "integrity": "sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w==", - "license": "MIT", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/flattie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", @@ -5030,6 +4683,12 @@ "node": ">=8" } }, + "node_modules/fnv1a-64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/fnv1a-64/-/fnv1a-64-0.1.2.tgz", + "integrity": "sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==", + "license": "MIT" + }, "node_modules/fontace": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", @@ -5081,10 +4740,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fzf": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", + "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", + "license": "BSD-3-Clause" + }, "node_modules/get-tsconfig": { - "version": "5.0.0-beta.4", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", - "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "version": "5.0.0-beta.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.6.tgz", + "integrity": "sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -5127,20 +4792,31 @@ } }, "node_modules/h3": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", - "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.7.tgz", + "integrity": "sha512-00SVloxtViij9/JBsc/Ayd9H7GLXWB0KRfkReWG0dDwuUFKUKRrNGM3Csu+KZgppi5DD8Y1KpnfjtudWaRCoiQ==", "license": "MIT", "dependencies": { - "cookie-es": "^1.2.3", - "crossws": "^0.3.5", - "defu": "^6.1.6", - "destr": "^2.0.5", - "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.4", - "radix3": "^1.1.2", - "ufo": "^1.6.3", - "uncrypto": "^0.1.3" + "rou3": "^1.0.0", + "srvx": "^1.0.6" + }, + "bin": { + "h3": "bin/h3.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "crossws": "^0.4.12", + "ocache": ">=0.3.0" + }, + "peerDependenciesMeta": { + "crossws": { + "optional": true + }, + "ocache": { + "optional": true + } } }, "node_modules/hachure-fill": { @@ -5553,15 +5229,15 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", "license": "BSD-2-Clause" }, "node_modules/i18next": { - "version": "26.4.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.2.tgz", - "integrity": "sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==", + "version": "26.5.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.4.tgz", + "integrity": "sha512-Wg0O1u2xmnsve9gICDwGekLkMe9SIC7+ZiuLYfWUGb/A3TQdRKygQ4OY9ros4raf5D66ZnW9JMrTDtGP2b8a7Q==", "funding": [ { "type": "individual", @@ -5774,28 +5450,28 @@ "license": "MIT" }, "node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.18.10.tgz", + "integrity": "sha512-/B6p9eY9DX7aHBfpkHdpirDTZ5QH9xTwL9y837PWuzuv41O5jFdqNQwVQEQsimY0/iVNkwBY4+4hMfhQ7d4Dbw==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], "license": "MIT", "dependencies": { - "commander": "^8.3.0" + "commander": "^15.0.0" }, "bin": { "katex": "cli.js" } }, "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=22.12.0" } }, "node_modules/khroma": { @@ -6092,34 +5768,10 @@ "node": "20 || >=22" } }, - "node_modules/magic-regexp": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz", - "integrity": "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==", - "license": "MIT", - "dependencies": { - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12", - "mlly": "^1.7.2", - "regexp-tree": "^0.1.27", - "type-level-regexp": "~0.1.17", - "ufo": "^1.5.4", - "unplugin": "^2.0.0" - } - }, - "node_modules/magic-regexp/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/magic-string": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", - "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" @@ -6186,9 +5838,9 @@ } }, "node_modules/mdast-util-directive": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", - "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.1.tgz", + "integrity": "sha512-Gqpa3MHorXYEZ0VfbroyvBlVKUfz371V6mIERqOYMcb0W1D3LwmFq1ZW4O2TDrbn+4ZmGLwMWeFRJgYZ+bc5fQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -6197,6 +5849,7 @@ "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", + "micromark-util-character": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" @@ -6207,9 +5860,9 @@ } }, "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.3.tgz", + "integrity": "sha512-xPgpDNl0/OXHsI7TlaIs22lWnH3KlpvZdXG1ET/m/YT2Hhdkx8lJV4kLphE6l9simyM4KPDFBwas08BgEob6Jw==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -6223,9 +5876,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz", + "integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -6460,9 +6113,9 @@ } }, "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.0.tgz", + "integrity": "sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -6470,8 +6123,10 @@ "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", + "micromark-util-character": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" }, @@ -6500,16 +6155,17 @@ "license": "CC0-1.0" }, "node_modules/mermaid": { - "version": "11.17.2", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.17.2.tgz", - "integrity": "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.2.0.tgz", + "integrity": "sha512-w7nr/SFrLO7BaVTs3dQTvYSV6nYSBj0Yy9DjBhsIJtAcHqRzXSq4Pg32ujHZ/AdWsAMNEvvhbm5dt9E2tOiHzw==", "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.2.1", + "@mermaid-js/parser": "^2.0.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", + "chevrotain": "~13.2.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", @@ -6517,22 +6173,25 @@ "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", - "dompurify": "^3.3.3", + "dompurify": "^3.4.12", + "elkjs": "^0.9.3", "es-toolkit": "^1.45.1", - "fastdom": "1.0.12", - "katex": "^0.16.47", + "katex": "^0.18.0", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", "funding": [ { "type": "GitHub Sponsors", @@ -6555,6 +6214,7 @@ "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", @@ -6565,9 +6225,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", "funding": [ { "type": "GitHub Sponsors", @@ -6584,18 +6244,19 @@ "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", + "micromark-factory-space": "^2.1.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "micromark-util-types": "^2.0.3" } }, "node_modules/micromark-extension-directive": { @@ -6911,9 +6572,9 @@ } }, "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", "funding": [ { "type": "GitHub Sponsors", @@ -7095,6 +6756,25 @@ "micromark-util-symbol": "^2.0.0" } }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, "node_modules/micromark-util-encode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", @@ -7250,9 +6930,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", "funding": [ { "type": "GitHub Sponsors", @@ -7289,18 +6969,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -7317,9 +6985,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -7396,9 +7064,9 @@ } }, "node_modules/obug": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", - "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" @@ -7419,12 +7087,6 @@ "ufo": "^1.6.1" } }, - "node_modules/ohash": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", - "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", - "license": "MIT" - }, "node_modules/oniguruma-parser": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", @@ -7443,12 +7105,12 @@ } }, "node_modules/oxc-parser": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.150.0.tgz", - "integrity": "sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.153.0.tgz", + "integrity": "sha512-cEnpyyV1OJRXv++SFNw0z8i+2o/cr88rVLaOYJafELDoHM5yHvCAbwGRLvxuany6SNLcbblSMUyZaGzH+quSYg==", "license": "MIT", "dependencies": { - "@oxc-project/types": "^0.150.0" + "@oxc-project/types": "^0.153.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -7457,37 +7119,56 @@ "url": "https://github.com/sponsors/oxc-project" }, "optionalDependencies": { - "@oxc-parser/binding-android-arm-eabi": "0.150.0", - "@oxc-parser/binding-android-arm64": "0.150.0", - "@oxc-parser/binding-darwin-arm64": "0.150.0", - "@oxc-parser/binding-darwin-x64": "0.150.0", - "@oxc-parser/binding-freebsd-x64": "0.150.0", - "@oxc-parser/binding-linux-arm-gnueabihf": "0.150.0", - "@oxc-parser/binding-linux-arm-musleabihf": "0.150.0", - "@oxc-parser/binding-linux-arm64-gnu": "0.150.0", - "@oxc-parser/binding-linux-arm64-musl": "0.150.0", - "@oxc-parser/binding-linux-ppc64-gnu": "0.150.0", - "@oxc-parser/binding-linux-riscv64-gnu": "0.150.0", - "@oxc-parser/binding-linux-riscv64-musl": "0.150.0", - "@oxc-parser/binding-linux-s390x-gnu": "0.150.0", - "@oxc-parser/binding-linux-x64-gnu": "0.150.0", - "@oxc-parser/binding-linux-x64-musl": "0.150.0", - "@oxc-parser/binding-openharmony-arm64": "0.150.0", - "@oxc-parser/binding-win32-arm64-msvc": "0.150.0", - "@oxc-parser/binding-win32-ia32-msvc": "0.150.0", - "@oxc-parser/binding-win32-x64-msvc": "0.150.0" + "@oxc-parser/binding-android-arm-eabi": "0.153.0", + "@oxc-parser/binding-android-arm64": "0.153.0", + "@oxc-parser/binding-darwin-arm64": "0.153.0", + "@oxc-parser/binding-darwin-x64": "0.153.0", + "@oxc-parser/binding-freebsd-x64": "0.153.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.153.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.153.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.153.0", + "@oxc-parser/binding-linux-arm64-musl": "0.153.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.153.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.153.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.153.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.153.0", + "@oxc-parser/binding-linux-x64-gnu": "0.153.0", + "@oxc-parser/binding-linux-x64-musl": "0.153.0", + "@oxc-parser/binding-openharmony-arm64": "0.153.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.153.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.153.0", + "@oxc-parser/binding-win32-x64-msvc": "0.153.0" + } + }, + "node_modules/oxc-parser/node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" } }, "node_modules/oxc-walker": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-0.7.0.tgz", - "integrity": "sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-1.1.1.tgz", + "integrity": "sha512-Hwd7dq28zu/Er99+bpWp16CGwFrhyalJh0W3JOB7XpPvgWo3MqMi2ksWGKuzzA9zt50mJ2pIUwR5lpAdZ9ACNQ==", "license": "MIT", - "dependencies": { - "magic-regexp": "^0.10.0" - }, "peerDependencies": { - "oxc-parser": ">=0.98.0" + "@oxc-project/types": ">=0.98.0", + "oxc-parser": ">=0.98.0", + "rolldown": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@oxc-project/types": { + "optional": true + }, + "oxc-parser": { + "optional": true + }, + "rolldown": { + "optional": true + } } }, "node_modules/p-limit": { @@ -7506,9 +7187,9 @@ } }, "node_modules/p-queue": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", - "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.5.tgz", + "integrity": "sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==", "license": "MIT", "dependencies": { "eventemitter3": "^5.0.4", @@ -7540,9 +7221,9 @@ "license": "BlueOak-1.0.0" }, "node_modules/package-manager-detector": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", - "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", "license": "MIT" }, "node_modules/pagefind": { @@ -7618,18 +7299,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/path-data-parser": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", @@ -7697,17 +7366,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", @@ -7725,9 +7383,9 @@ } }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", "funding": [ { "type": "opencollective", @@ -7744,18 +7402,18 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz", + "integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==", "funding": [ { "type": "opencollective", @@ -7768,19 +7426,19 @@ ], "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.1.1" + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": ">=12.0" + "node": ">=18.0" }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.7.tgz", + "integrity": "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -7790,6 +7448,21 @@ "node": ">=4" } }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -7944,15 +7617,6 @@ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, - "node_modules/regexp-tree": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", - "license": "MIT", - "bin": { - "regexp-tree": "bin/regexp-tree" - } - }, "node_modules/rehype": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", @@ -7970,12 +7634,12 @@ } }, "node_modules/rehype-expressive-code": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.2.tgz", - "integrity": "sha512-jvjQpsfWKBquDrEl5YQA3CJybRRIuDOYsui1BJTs5oKDUK8pPkJSSxY8FjxNlIFIdtgrTjC1zuahuY4Cc1j9SQ==", + "version": "0.44.3", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.3.tgz", + "integrity": "sha512-vaVuQPobFXvmtcHXQ5uqXZlzuuPnLyIuZcX/03sX1U/Qfj9fZrPYI9w66OSr/PUx6alzRYmreEwgYBbsVx8HsA==", "license": "MIT", "dependencies": { - "expressive-code": "^0.44.2" + "expressive-code": "^0.44.3" } }, "node_modules/rehype-parse": { @@ -8226,12 +7890,12 @@ "license": "Unlicense" }, "node_modules/rolldown": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", - "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.150.0", + "@oxc-project/types": "=0.155.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -8241,28 +7905,31 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.9", - "@rolldown/binding-android-arm64": "1.2.9", - "@rolldown/binding-darwin-arm64": "1.2.9", - "@rolldown/binding-darwin-x64": "1.2.9", - "@rolldown/binding-freebsd-x64": "1.2.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", - "@rolldown/binding-linux-arm64-gnu": "1.2.9", - "@rolldown/binding-linux-arm64-musl": "1.2.9", - "@rolldown/binding-linux-ppc64-gnu": "1.2.9", - "@rolldown/binding-linux-s390x-gnu": "1.2.9", - "@rolldown/binding-linux-x64-gnu": "1.2.9", - "@rolldown/binding-linux-x64-musl": "1.2.9", - "@rolldown/binding-openharmony-arm64": "1.2.9", - "@rolldown/binding-win32-arm64-msvc": "1.2.9", - "@rolldown/binding-win32-x64-msvc": "1.2.9" + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" } }, "node_modules/rou3": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz", - "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==", - "license": "MIT" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-1.0.0.tgz", + "integrity": "sha512-ySeaQkSrua+ut9g+65I7FHqOFBaY8tV0FmccXItoq0q21se32Q3u4YiW7/RqXSosWgYJaaKwRHXy3BST9PeCNQ==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } }, "node_modules/roughjs": { "version": "4.6.6", @@ -8333,9 +8000,9 @@ } }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", @@ -8349,31 +8016,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { @@ -8403,17 +8070,17 @@ } }, "node_modules/shiki": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", - "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.5.0.tgz", + "integrity": "sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==", "license": "MIT", "dependencies": { - "@shikijs/core": "4.4.3", - "@shikijs/engine-javascript": "4.4.3", - "@shikijs/engine-oniguruma": "4.4.3", - "@shikijs/langs": "4.4.3", - "@shikijs/themes": "4.4.3", - "@shikijs/types": "4.4.3", + "@shikijs/core": "4.5.0", + "@shikijs/engine-javascript": "4.5.0", + "@shikijs/engine-oniguruma": "4.5.0", + "@shikijs/langs": "4.5.0", + "@shikijs/themes": "4.5.0", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" }, @@ -8459,9 +8126,9 @@ } }, "node_modules/smol-toml": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", - "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -8471,18 +8138,18 @@ } }, "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 12" } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8499,9 +8166,9 @@ } }, "node_modules/srvx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/srvx/-/srvx-1.0.5.tgz", - "integrity": "sha512-KvSKRpgPG/oaq3cyT614OQ2bAa7DynuGamivCZeoZzIUULOdbQz6wUtnnVuC4+DCx2Zglzo8v5gBYmWIWYhDxA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/srvx/-/srvx-1.0.6.tgz", + "integrity": "sha512-QeTKgAhk8OSsfeq60bl3wuVs9PmBftzmk7owBInA/F1YQne+TRw0UcPymNpURldAmx8hfmM2PNNGH/zgo3GpLA==", "license": "MIT", "bin": { "srvx": "bin/srvx.mjs" @@ -8618,10 +8285,10 @@ "@astrojs/starlight": ">=0.41.0" } }, - "node_modules/strictdom": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strictdom/-/strictdom-1.0.1.tgz", - "integrity": "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==", + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "license": "MIT" }, "node_modules/stringify-entities": { @@ -8675,9 +8342,9 @@ } }, "node_modules/supports-hyperlinks": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", - "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.6.0.tgz", + "integrity": "sha512-FL/q/YePS4tFFTNiBOR+zPk2ZrKheqNtmTJ/9OUZ4e1wKHJofDbGQSmSsxS0XNnabSkqexryCaXGvEKz1vf9UQ==", "license": "MIT", "dependencies": { "has-flag": "^5.0.1", @@ -8715,6 +8382,15 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/svgo/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/terminal-link": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-5.0.0.tgz", @@ -8738,9 +8414,9 @@ "license": "MIT" }, "node_modules/tinyclip": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz", - "integrity": "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-1.0.3.tgz", + "integrity": "sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==", "license": "MIT", "engines": { "node": "^16.14.0 || >= 17.3.0" @@ -8771,6 +8447,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyps": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/tinyps/-/tinyps-0.5.1.tgz", + "integrity": "sha512-B6Ykif5HflC3AdDkS4Zst/2muYVfeXoVMy+KorqH80s/1x4vunIORpNySH681X7fjj+dFM9MDKD6/Hsccjb87Q==", + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -8807,12 +8489,6 @@ "license": "0BSD", "optional": true }, - "node_modules/type-level-regexp": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz", - "integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -8875,18 +8551,18 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", - "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", "license": "MIT", "engines": { "node": ">=22.19.0" } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/unified": { @@ -8909,13 +8585,14 @@ } }, "node_modules/unifont": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz", - "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-1.0.3.tgz", + "integrity": "sha512-XGnEwj5sWdCbiBX01mWLSBncS0yMRjnLygau3IfdOlfjMacNIQvTGW4VH7pWBq+gGWArfSef/CensMLjMmIr/A==", "license": "MIT", "dependencies": { "css-tree": "^3.1.0", - "ohash": "^2.0.11", + "fnv1a-64": "^0.1.2", + "std-env": "^4.2.0", "undici": "^8.0.0" } }, @@ -9056,37 +8733,37 @@ } }, "node_modules/unocss": { - "version": "66.10.5", - "resolved": "https://registry.npmjs.org/unocss/-/unocss-66.10.5.tgz", - "integrity": "sha512-hts6uxRjtEdOLmdmo0Mbh5k3jrHsjsQlU3rFJdtob+wu57iM5b+d70fnxqMWe7yF/OBeiZKW6H+ti0qHNJBKPw==", - "license": "MIT", - "dependencies": { - "@unocss/cli": "66.10.5", - "@unocss/core": "66.10.5", - "@unocss/preset-attributify": "66.10.5", - "@unocss/preset-icons": "66.10.5", - "@unocss/preset-mini": "66.10.5", - "@unocss/preset-tagify": "66.10.5", - "@unocss/preset-typography": "66.10.5", - "@unocss/preset-uno": "66.10.5", - "@unocss/preset-web-fonts": "66.10.5", - "@unocss/preset-wind": "66.10.5", - "@unocss/preset-wind3": "66.10.5", - "@unocss/preset-wind4": "66.10.5", - "@unocss/rollup": "66.10.5", - "@unocss/transformer-attributify-jsx": "66.10.5", - "@unocss/transformer-compile-class": "66.10.5", - "@unocss/transformer-directives": "66.10.5", - "@unocss/transformer-variant-group": "66.10.5", - "@unocss/vite": "66.10.5" + "version": "66.11.0", + "resolved": "https://registry.npmjs.org/unocss/-/unocss-66.11.0.tgz", + "integrity": "sha512-fUhpMwVzMVxy4IcaL2sXjJV5Q3UpEKv3U4wfYioHmv7+C07Y8WAmjVgROoXKAUKs+pWlQKwtPMw4ZjMpQX8blA==", + "license": "MIT", + "dependencies": { + "@unocss/cli": "66.11.0", + "@unocss/core": "66.11.0", + "@unocss/preset-attributify": "66.11.0", + "@unocss/preset-icons": "66.11.0", + "@unocss/preset-mini": "66.11.0", + "@unocss/preset-tagify": "66.11.0", + "@unocss/preset-typography": "66.11.0", + "@unocss/preset-uno": "66.11.0", + "@unocss/preset-web-fonts": "66.11.0", + "@unocss/preset-wind": "66.11.0", + "@unocss/preset-wind3": "66.11.0", + "@unocss/preset-wind4": "66.11.0", + "@unocss/rollup": "66.11.0", + "@unocss/transformer-attributify-jsx": "66.11.0", + "@unocss/transformer-compile-class": "66.11.0", + "@unocss/transformer-directives": "66.11.0", + "@unocss/transformer-variant-group": "66.11.0", + "@unocss/vite": "66.11.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@unocss/astro": "66.10.5", - "@unocss/postcss": "66.10.5", - "@unocss/webpack": "66.10.5" + "@unocss/astro": "66.11.0", + "@unocss/postcss": "66.11.0", + "@unocss/webpack": "66.11.0" }, "peerDependenciesMeta": { "@unocss/astro": { @@ -9100,21 +8777,6 @@ } } }, - "node_modules/unplugin": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", - "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=18.12.0" - } - }, "node_modules/unplugin-utils": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", @@ -9132,19 +8794,19 @@ } }, "node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.6.tgz", + "integrity": "sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==", "license": "MIT", "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", + "h3": "^1.15.11", + "lru-cache": "^11.5.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", - "ufo": "^1.6.3" + "ufo": "^1.6.4" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", @@ -9155,7 +8817,7 @@ "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", @@ -9227,6 +8889,32 @@ } } }, + "node_modules/unstorage/node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/unstorage/node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, "node_modules/url-extras": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/url-extras/-/url-extras-0.1.0.tgz", @@ -9246,9 +8934,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", - "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz", + "integrity": "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -9259,9 +8947,9 @@ } }, "node_modules/verkit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.4.1.tgz", - "integrity": "sha512-mMVzj0TXExtVdlDEq+Mzp0eyOuyznNpFobNM3uAqe3HVm/Ps2c+u17BligMkZjZCA6EiXpoon8iRTer3iu40SQ==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", "license": "MIT", "engines": { "node": ">=18.12.0" @@ -9313,15 +9001,15 @@ } }, "node_modules/vite": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", - "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.6", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "bin": { @@ -9338,7 +9026,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -9408,6 +9096,49 @@ } } }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.3.tgz", + "integrity": "sha512-qETYf9PCl1lVfwR+oBmMfc4vP2nqLNA35ABxorw3j06+xlKRgDVxbLgKm4Y/xPL0YWdeDjLoI4ImICIT3nB0zQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.1.2.tgz", + "integrity": "sha512-pXPWde3RytdeBVEs6PulOOT5OpeXC5GkWRpcT11f5rNn41jviQsAYPyAqJ/0oVGETQLKPJtYbHNiMPlfLb0NTQ==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.18.4" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.4.tgz", + "integrity": "sha512-CCXbyr99dafRJzJH/0Ryk7ly5T8XPmSR0//OuF2w4m+9JiOin7zcAT5iET8GRE7Z9taFBejb3GbM0a0mbjdbrg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.3", + "vscode-languageserver-types": "3.18.4" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.15.tgz", + "integrity": "sha512-Gsfr9S8LU3S6885cS+NaleasJ0n3V5y/SOTqHCifQiXWFw3G9w9/YItDfwHrlCz+dSZWWZ8ilYVMcJe7Sz7Sxw==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.4.tgz", + "integrity": "sha512-nQsacoiijvl0fRf2q28uFDrAVwRhdCa3QveiF7/CX0r18mLR5FU/1WskSsCKOjmEbtteKUnY8x8Yzxop+k0zwg==", + "license": "MIT" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -9418,12 +9149,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9482,9 +9207,9 @@ } }, "node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/docs/package.json b/docs/package.json index 1ac4bee6..3304d37d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -20,7 +20,7 @@ "astro": "^7.2.10", "astro-contributors": "^0.10.0", "astro-mermaid": "^2.1.0", - "mermaid": "^11.17.2", + "mermaid": "^12.2.0", "sharp": "^0.35.3", "starlight-announcement": "^2.0.0", "starlight-base-path": "^0.2.1", diff --git a/docs/scripts/generate-api.mjs b/docs/scripts/generate-api.mjs index d647c484..109dd80a 100644 --- a/docs/scripts/generate-api.mjs +++ b/docs/scripts/generate-api.mjs @@ -8,6 +8,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const docsRoot = path.resolve(__dirname, ".."); const xmlDir = path.join(docsRoot, "doxygen_xml"); +const cpputilsSymbols = {}; + if (!fs.existsSync(xmlDir)) { console.error(`[generate-api] Error: ${xmlDir} does not exist. Run doxygen first.`); process.exit(1); @@ -20,9 +22,9 @@ function parseXml(xmlString) { const stack = [root]; parser.onopentag = (node) => { - const el = { tag: node.name, attributes: node.attributes || {}, children: [], text: "" }; - stack[stack.length - 1].children.push(el); - stack.push(el); + const element = { tag: node.name, attributes: node.attributes || {}, children: [], text: "" }; + stack[stack.length - 1].children.push(element); + stack.push(element); }; parser.ontext = (text) => { @@ -78,117 +80,45 @@ function cleanSignature(str) { return decodeEntities(str).replace(/\s+/g, " ").trim(); } -function renderDocNode(node, lang = "en", refMap = null) { - if (!node) return ""; - if (node.tag === "#text") return node.text || ""; - let result = ""; - - for (const child of node.children || []) { - if (child.tag === "#text") result += child.text || ""; - else if (child.tag === "para") { - const paraText = renderDocNode(child, lang, refMap).trim(); - if (paraText) result += `${paraText}\n\n`; - } else if (child.tag === "parameterlist") { - const isTemplate = child.attributes.kind === "templateparam"; - const colName = isTemplate - ? (lang === "fr" ? "Paramètre template" : "Template parameter") - : (lang === "fr" ? "Paramètre" : "Parameter"); - - const items = findChildren(child, "parameteritem"); - if (items.length > 0) { - result += `| ${colName} | Description |\n`; - result += `| :--- | :--- |\n`; - for (const item of items) { - const nameNode = findChild(findChild(item, "parameternamelist"), "parametername"); - const descNode = findChild(findChild(item, "parameterdescription"), "para"); - const name = getAllText(nameNode).trim(); - const desc = renderDocNode(descNode, lang, refMap).trim().replace(/\n+/g, " "); - if (name) result += `| \`${name}\` | ${desc} |\n`; - } - result += "\n"; - } - } else if (child.tag === "simplesect") { - const kind = child.attributes.kind; - if (kind === "see") { - const paras = findChildren(child, "para"); - const targetParas = paras.length > 0 ? paras : [child]; - for (const para of targetParas) { - const allParaText = getAllText(para).trim(); - const ulink = findChild(para, "ulink") || findChildren(para, "para").map((p) => findChild(p, "ulink")).find(Boolean); - const rawUrl = ulink?.attributes?.url || (allParaText.match(/https?:\/\/[^\s)]+/)?.[0] || ""); - - const docMatch = rawUrl.match(/(?:https?:\/\/[^/]+)?\/?(?:CppUtils)?\/(fr|en)\/([^)\s]+)/); - if (docMatch) { - const linkLang = docMatch[1]; - const docPath = docMatch[2].replace(/\/+$/, ""); - if (linkLang !== lang) continue; - - const labelMatch = allParaText.match(/\(([^)]+)\)/); - const label = labelMatch ? labelMatch[1].trim() : (lang === "fr" ? "Guide associé" : "Related guide"); - const targetUrl = `/${lang}/${docPath}/`; - - const noteTitle = lang === "fr" ? "Guide associé" : "Related guide"; - const noteText = lang === "fr" - ? `Consultez le [${label}](${targetUrl}) pour une explication détaillée et des exemples d'utilisation.` - : `Check out the [${label}](${targetUrl}) for detailed explanations and usage examples.`; - - result += `\n:::note[${noteTitle}]\n${noteText}\n:::\n\n`; - continue; - } - - const content = renderDocNode(para, lang, refMap).trim(); - if (content) { - result += `${lang === "fr" ? "\n**Voir aussi :** " : "\n**See also:** "}${content}\n\n`; - } - } - continue; - } - const content = renderDocNode(child, lang, refMap).trim(); - if (kind === "return") result += `${lang === "fr" ? "\n**Retourne :** " : "\n**Returns:** "}${content}\n\n`; - else if (kind === "note") result += `\n:::note\n${content}\n:::\n\n`; - else if (kind === "tip" || kind === "remark" || kind === "remarks") result += `\n:::tip\n${content}\n:::\n\n`; - else if (kind === "warning" || kind === "attention") result += `\n:::caution\n${content}\n:::\n\n`; - else if (kind === "danger" || kind === "error") result += `\n:::danger\n${content}\n:::\n\n`; - else result += `${content}\n\n`; - } else if (child.tag === "ulink") { - const url = child.attributes?.url || ""; - const text = renderDocNode(child, lang, refMap).trim() || url; - result += `[${text}](${url})`; - } else if (child.tag === "ref") { - const text = getAllText(child).trim(); - const refId = child.attributes?.refid; - let target = refMap?.get(refId); - if (!target && refId) { - const parentRefId = refId.replace(/_1[a-zA-Z0-9]+$/, ""); - target = refMap?.get(parentRefId); - } - - if (target) result += `[\`${text}\`](${target.url})`; - else if (child.attributes?.kindref === "compound" && refId?.startsWith("namespace")) result += text; - else result += `\`${text}\``; - } else if (child.tag === "computeroutput") result += `\`${getAllText(child).trim()}\``; - else if (child.tag === "programlisting") { - const codeLines = findChildren(child, "codeline").map((lineNode) => getAllText(lineNode)); - const code = codeLines.length > 0 ? codeLines.join("\n") : getAllText(child); - result += `\n\`\`\`cpp showLineNumbers\n${code.trim()}\n\`\`\`\n\n`; - } else result += renderDocNode(child, lang, refMap); - } +// Collapse excessive whitespace in nested template brackets (e.g. vector< int > -> vector) +function cleanTemplateSpaces(str) { + if (!str) return ""; + let result = str; + let previous; + do { + previous = result; + result = result + .replace(/<\s+/g, "<") + .replace(/\s+>/g, ">") + .replace(/>\s+>/g, ">>") + .replace(/\s*,\s*/g, ", "); + } while (result !== previous); + return result; +} - return decodeEntities(result); +function cleanCppType(type) { + if (!type) return ""; + return cleanTemplateSpaces(decodeEntities(type)) + .replace(/\s+/g, " ") + .replace(/(\bconst\s+[\w:<>]+|[\w:<>]+)\s+([&*]+)/g, "$1$2") + .trim(); } -function extractTemplateParams(node) { - const tplNode = findChild(node, "templateparamlist"); - if (!tplNode) return ""; - const params = findChildren(tplNode, "param").map((p) => { - const type = cleanSignature(getAllText(findChild(p, "type"))); - const declname = cleanSignature(getAllText(findChild(p, "declname"))); - const defval = cleanSignature(getAllText(findChild(p, "defval"))); - let str = [type, declname].filter(Boolean).join(" "); - if (defval) str += ` = ${defval}`; - return str; - }); - return params.length > 0 ? `template <${params.join(", ")}>` : ""; +// Normalize parameter formatting (spacing around references, pointers, and variadic packs) +function cleanCppArgs(args) { + if (!args) return "()"; + return cleanTemplateSpaces(decodeEntities(args)) + .replace(/\s+/g, " ") + .replace(/\s*=\s*(delete|default|0)\b/g, " = $1") + .replace(/(\bconst\s+[\w:<>]+|[\w:<>]+)\s+([&*]+)\s*(?:\.\.\.\s*)?(\w+)/g, (match, type, ref, name) => { + const hasEllipsis = match.includes("..."); + return `${type}${ref}${hasEllipsis ? "... " : " "}${name}`; + }) + .replace(/(\bconst\s+[\w:<>]+|[\w:<>]+)\s+([&*]+)\s*(?:\.\.\.\s*)?([,)])/g, (match, type, ref, end) => { + const hasEllipsis = match.includes("..."); + return `${type}${ref}${hasEllipsis ? "..." : ""}${end}`; + }) + .trim(); } function sanitizeFileName(name) { @@ -216,19 +146,27 @@ function sanitizeHtmlId(name) { .replace(/^_|_$/g, ""); } -function cleanTemplateSpaces(str) { - if (!str) return ""; - let res = str; - let prev; - do { - prev = res; - res = res - .replace(/<\s+/g, "<") - .replace(/\s+>/g, ">") - .replace(/>\s+>/g, ">>") - .replace(/\s*,\s*/g, ", "); - } while (res !== prev); - return res; +// Split qualified C++ name by "::" while respecting template and function parameter nesting +function splitQualifiedName(rawName) { + const parts = []; + let current = ""; + let depth = 0; + for (let i = 0; i < rawName.length; i++) { + const character = rawName[i]; + if (character === "<" || character === "(") { + depth++; + current += character; + } else if (character === ">" || character === ")") { + depth = Math.max(0, depth - 1); + current += character; + } else if (character === ":" && depth === 0 && rawName[i + 1] === ":") { + parts.push(current); + current = ""; + i++; + } else current += character; + } + if (current) parts.push(current); + return parts; } const sourceFilesCache = new Map(); @@ -241,6 +179,7 @@ function getSourceLines(filePath) { return sourceFilesCache.get(filePath); } +// Read export module declaration from C++20 module interface file function extractModuleFromSource(filePath) { if (!filePath) return null; const lines = getSourceLines(filePath); @@ -251,30 +190,25 @@ function extractModuleFromSource(filePath) { return null; } -function cleanCppType(type) { - if (!type) return ""; - return cleanTemplateSpaces(decodeEntities(type)) - .replace(/\s+/g, " ") - .replace(/(\bconst\s+[\w:]+|[\w:]+)\s+([&*]+)/g, "$1$2") - .trim(); +function isDetailNamespace(name) { + return /::(detail|internal|implementation)(::|$)/i.test(name); } -function cleanCppArgs(args) { - if (!args) return "()"; - return cleanTemplateSpaces(decodeEntities(args)) - .replace(/\s+/g, " ") - .replace(/\s*=\s*(delete|default|0)\b/g, " = $1") - .replace(/(\bconst\s+[\w:]+|[\w:]+)\s+([&*]+)\s*(?:\.\.\.\s*)?(\w+)/g, (match, type, ref, name) => { - const hasEllipsis = match.includes("..."); - return `${type}${ref}${hasEllipsis ? "... " : " "}${name}`; - }) - .replace(/(\bconst\s+[\w:]+|[\w:]+)\s+([&*]+)\s*(?:\.\.\.\s*)?([,)])/g, (match, type, ref, end) => { - const hasEllipsis = match.includes("..."); - return `${type}${ref}${hasEllipsis ? "..." : ""}${end}`; - }) - .trim(); +function formatConceptDeclaration(compounddef, symbolName) { + const initNode = findChild(compounddef, "initializer"); + let initText = cleanSignature(getAllText(initNode)).trim(); + if (initText) { + initText = initText.replace(new RegExp(`concept\\s+CppUtils::(?:[A-Za-z0-9_]+::)*${symbolName}\\b`), `concept ${symbolName}`); + if (!initText.endsWith(";")) initText += ";"; + initText = initText.replace(/^(template\s*<[^>]+>)\s*(concept\b)/, "$1\n$2"); + return cleanTemplateSpaces(initText); + } + const templateHeader = extractTemplateParams(compounddef); + const header = templateHeader ? `${templateHeader}\n` : ""; + return cleanTemplateSpaces(`${header}concept ${symbolName} = ...;`); } +// Scan tokens to find where a function signature terminates (either '{' body, ';' or constructor ':' initializer) function findDeclarationEnd(buffer) { let inString = false; let inChar = false; @@ -283,24 +217,24 @@ function findDeclarationEnd(buffer) { let seenParen = false; for (let i = 0; i < buffer.length; i++) { - const ch = buffer[i]; + const character = buffer[i]; // Escape handling for strings and chars if (inString || inChar) { - if (ch === "\\") { + if (character === "\\") { i++; continue; } - if (inString && ch === '"') inString = false; - else if (inChar && ch === "'") inChar = false; + if (inString && character === '"') inString = false; + else if (inChar && character === "'") inChar = false; continue; } - if (ch === '"') { + if (character === '"') { inString = true; continue; } - if (ch === "'") { + if (character === "'") { // Check for C++ digit separator (1'000 or 0xFF'FF) if (i > 0 && /[\da-fA-F]/.test(buffer[i - 1]) && i + 1 < buffer.length && /[\da-fA-F]/.test(buffer[i + 1])) continue; inChar = true; @@ -308,32 +242,32 @@ function findDeclarationEnd(buffer) { } // Skip single-line comments // ... - if (ch === "/" && buffer[i + 1] === "/") { - const newlineIdx = buffer.indexOf("\n", i); - if (newlineIdx === -1) break; - i = newlineIdx; + if (character === "/" && buffer[i + 1] === "/") { + const newlineIndex = buffer.indexOf("\n", i); + if (newlineIndex === -1) break; + i = newlineIndex; continue; } - if (ch === "(") { + if (character === "(") { depthParen++; seenParen = true; - } else if (ch === ")") { + } else if (character === ")") { if (depthParen > 0) depthParen--; - } else if (ch === "{") { + } else if (character === "{") { if (depthParen === 0 && depthBrace === 0) { // Check if this { belongs to a requires expression (requires requires { ... }) const prefix = buffer.slice(0, i).trimEnd(); if (/\brequires(\s*\([^)]*\))?$/.test(prefix)) depthBrace++; else return i; // Function body begins here! } else depthBrace++; - } else if (ch === "}") { + } else if (character === "}") { if (depthBrace > 0) depthBrace--; - } else if (ch === ";" && depthParen === 0 && depthBrace === 0) { + } else if (character === ";" && depthParen === 0 && depthBrace === 0) { // End of declaration without body (or = default / = delete) return i; } else if ( - ch === ":" && + character === ":" && depthParen === 0 && depthBrace === 0 && seenParen && @@ -348,16 +282,17 @@ function findDeclarationEnd(buffer) { return -1; } +// Re-read declaration from source code to preserve modern C++ syntax (trailing return types, requires clauses) function extractDeclarationFromSource(filePath, startLine, className, memberName) { if (!filePath || !fs.existsSync(filePath)) return null; const lines = getSourceLines(filePath); if (!lines || startLine < 1 || startLine > lines.length) return null; - let idx = startLine - 1; + let startLineIndex = startLine - 1; let buffer = ""; let foundName = false; - for (let i = idx; i < Math.min(lines.length, idx + 12); i++) { + for (let i = startLineIndex; i < Math.min(lines.length, startLineIndex + 12); i++) { const rawLine = lines[i]; buffer += (buffer ? " " : "") + rawLine.trim(); @@ -366,205 +301,363 @@ function extractDeclarationFromSource(filePath, startLine, className, memberName } else if (memberName.startsWith("~")) { if (buffer.includes("~")) foundName = true; } else { - const esc = memberName.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - if (new RegExp(`\\b${esc}\\b`).test(buffer)) foundName = true; + const escapedMemberName = memberName.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + if (new RegExp(`\\b${escapedMemberName}\\b`).test(buffer)) foundName = true; } if (foundName) { const endPos = findDeclarationEnd(buffer); if (endPos !== -1) { - let sig = buffer.slice(0, endPos).trim(); - + let declarationSignature = buffer.slice(0, endPos).trim(); // Strip inline keyword - sig = sig.replace(/\binline\s+/g, ""); + declarationSignature = declarationSignature.replace(/\binline\s+/g, ""); // Prepend className if (className) { - if (!sig.includes(`${className}::${memberName}`)) { + if (!declarationSignature.includes(`${className}::${memberName}`)) { if (memberName.startsWith("operator")) { const opPart = memberName.slice("operator".length).trim(); - const escOp = opPart.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - sig = sig.replace(new RegExp(`(? 0) { - const fromSrc = extractDeclarationFromSource(filePath, lineNum, className, fName); - if (fromSrc) return fromSrc; + if (className && filePath && lineNumber > 0) { + const declarationFromSource = extractDeclarationFromSource(filePath, lineNumber, className, functionName); + if (declarationFromSource) return declarationFromSource; } // Fallback to Doxygen attributes - const fType = cleanCppType(getAllText(findChild(f, "type"))); - const fArgs = cleanCppArgs(getAllText(findChild(f, "argsstring"))); - const qualifiedName = className ? `${className}::${fName}` : fName; - return `${fType ? fType + " " : ""}${qualifiedName}${fArgs || "()"};`; + const returnType = cleanCppType(getAllText(findChild(functionNode, "type"))); + const argumentsString = cleanCppArgs(getAllText(findChild(functionNode, "argsstring"))); + const qualifiedName = className ? `${className}::${functionName}` : functionName; + return `${returnType ? returnType + " " : ""}${qualifiedName}${argumentsString || "()"};`; } -function splitQualifiedName(rawName) { - const parts = []; - let current = ""; - let depth = 0; - for (let i = 0; i < rawName.length; i++) { - const ch = rawName[i]; - if (ch === "<" || ch === "(") { - depth++; - current += ch; - } else if (ch === ">" || ch === ")") { - depth = Math.max(0, depth - 1); - current += ch; - } else if (ch === ":" && depth === 0 && rawName[i + 1] === ":") { - parts.push(current); - current = ""; - i++; - } else current += ch; +// Extract and format template parameters into a C++ template <...> clause +function extractTemplateParams(node) { + const templateNode = findChild(node, "templateparamlist"); + if (!templateNode) return ""; + const params = findChildren(templateNode, "param").map((p) => { + const type = cleanSignature(getAllText(findChild(p, "type"))); + const declarationName = cleanSignature(getAllText(findChild(p, "declname"))); + const defaultValue = cleanSignature(getAllText(findChild(p, "defval"))); + let str = [type, declarationName].filter(Boolean).join(" "); + if (defaultValue) str += ` = ${defaultValue}`; + return str; + }); + return params.length > 0 ? `template <${params.join(", ")}>` : ""; +} + +function autolinkDocText(str, symbolCatalog) { + if (!str || !symbolCatalog) return str || ""; + return str.replace( + /\b([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)\s*(\([^)]*\))/g, + (fullMatch, symbol, args) => { + const target = symbolCatalog.get(symbol); + if (target) { + return `[\`${symbol}${args}\`](${target.url})`; + } + return fullMatch; + } + ); +} + +function renderDocNode(node, lang = "en", refMap = null, symbolCatalog = null) { + if (!node) return ""; + if (node.tag === "#text") return autolinkDocText(node.text || "", symbolCatalog); + let result = ""; + + for (const child of node.children || []) { + if (child.tag === "#text") { + result += autolinkDocText(child.text || "", symbolCatalog); + } else if (child.tag === "para") { + const paraText = renderDocNode(child, lang, refMap, symbolCatalog).trim(); + if (paraText) result += `${paraText}\n\n`; + } else if (child.tag === "parameterlist") { + // Render function or template parameter documentation as markdown tables + const isTemplate = child.attributes.kind === "templateparam"; + const colName = isTemplate + ? (lang === "fr" ? "Paramètre template" : "Template parameter") + : (lang === "fr" ? "Paramètre" : "Parameter"); + + const items = findChildren(child, "parameteritem"); + if (items.length > 0) { + result += `| ${colName} | Description |\n`; + result += `| :--- | :--- |\n`; + for (const item of items) { + const nameNode = findChild(findChild(item, "parameternamelist"), "parametername"); + const descriptionNode = findChild(findChild(item, "parameterdescription"), "para"); + const name = getAllText(nameNode).trim(); + const description = renderDocNode(descriptionNode, lang, refMap, symbolCatalog).trim().replace(/\n+/g, " "); + if (name) result += `| \`${name}\` | ${description} |\n`; + } + result += "\n"; + } + } else if (child.tag === "simplesect") { + const kind = child.attributes.kind; + if (kind === "see") { + // Transform @see references into guide callouts or external markdown links + const paras = findChildren(child, "para"); + const targetParas = paras.length > 0 ? paras : [child]; + for (const para of targetParas) { + const allParaText = getAllText(para).trim(); + const ulink = findChild(para, "ulink") || findChildren(para, "para").map((p) => findChild(p, "ulink")).find(Boolean); + const rawUrl = (ulink?.attributes?.url || (allParaText.match(/https?:\/\/[^\s)]+/)?.[0] || "")) + .replace(/–/g, "--") + .replace(/—/g, "---") + .replace(/\u2013/g, "--") + .replace(/\u2014/g, "---"); + + const docMatch = rawUrl.match(/(?:https?:\/\/[^/]+)?\/?(?:CppUtils)?\/(fr|en)\/([^)\s]+)/); + if (docMatch) { + const linkLang = docMatch[1]; + const docPath = docMatch[2]; + if (linkLang !== lang) continue; + + let cleanPath = docPath; + let cleanHash = ""; + const hashIndex = cleanPath.indexOf("#"); + if (hashIndex !== -1) { + cleanHash = cleanPath.slice(hashIndex).replace(/\/+$/, ""); + cleanPath = cleanPath.slice(0, hashIndex); + } + cleanPath = cleanPath.replace(/\/+$/, ""); + const targetUrl = cleanHash ? `/${linkLang}/${cleanPath}/${cleanHash}` : `/${linkLang}/${cleanPath}/`; + + const labelMatch = allParaText.match(/\(([^)]+)\)/); + const label = labelMatch ? labelMatch[1].trim() : (lang === "fr" ? "Guide associé" : "Related guide"); + + const noteTitle = lang === "fr" ? "Guide associé" : "Related guide"; + const noteText = lang === "fr" + ? `Consultez la section [${label}](${targetUrl}) pour une explication détaillée et des exemples d'utilisation.` + : `See the [${label}](${targetUrl}) section for detailed explanations and usage examples.`; + + result += `\n:::note[${noteTitle}]\n${noteText}\n:::\n\n`; + continue; + } + + const firstParen = allParaText.indexOf("("); + const lastParen = allParaText.lastIndexOf(")"); + const label = (firstParen !== -1 && lastParen > firstParen) + ? allParaText.slice(firstParen + 1, lastParen).trim() + : ""; + if (ulink && label) { + const url = (ulink.attributes?.url || "") + .replace(/–/g, "--") + .replace(/—/g, "---") + .replace(/\u2013/g, "--") + .replace(/\u2014/g, "---"); + const safeLabel = label.replace(/<([^>]+)>/g, "`<$1>`"); + result += `${lang === "fr" ? "\n**Voir aussi :** " : "\n**See also:** "}[${safeLabel}](${url})\n\n`; + continue; + } + + const content = renderDocNode(para, lang, refMap, symbolCatalog).trim(); + if (content) { + result += `${lang === "fr" ? "\n**Voir aussi :** " : "\n**See also:** "}${content}\n\n`; + } + } + continue; + } + // Map generic Doxygen command sections (@return, @note, @warning, etc.) to Starlight markdown callouts + const content = renderDocNode(child, lang, refMap, symbolCatalog).trim(); + if (kind === "return") result += `${lang === "fr" ? "\n**Retourne :** " : "\n**Returns:** "}${content}\n\n`; + else if (kind === "note") result += `\n:::note\n${content}\n:::\n\n`; + else if (kind === "tip" || kind === "remark" || kind === "remarks") result += `\n:::tip\n${content}\n:::\n\n`; + else if (kind === "warning" || kind === "attention") result += `\n:::caution\n${content}\n:::\n\n`; + else if (kind === "danger" || kind === "error") result += `\n:::danger\n${content}\n:::\n\n`; + else result += `${content}\n\n`; + } else if (child.tag === "ulink") { + // Render external hyperlinks + const url = (child.attributes?.url || "") + .replace(/–/g, "--") + .replace(/—/g, "---") + .replace(/\u2013/g, "--") + .replace(/\u2014/g, "---"); + const text = renderDocNode(child, lang, refMap, symbolCatalog).trim() || url; + result += `[${text}](${url})`; + } else if (child.tag === "ref") { + // Resolve cross-references to types, classes, and member methods + const text = getAllText(child).trim(); + const refId = child.attributes?.refid; + let target = refMap?.get(refId); + if (!target && refId) { + const parentRefId = refId.replace(/_1[a-zA-Z0-9]+$/, ""); + target = refMap?.get(parentRefId); + if (target && child.attributes?.kindref === "member") { + const memberName = sanitizeHtmlId(text.replace(/\(\)$/, "")); + target = { url: `${target.url}#${memberName}`, name: target.name }; + } + } + + if (target) result += `[\`${text}\`](${target.url})`; + else if (child.attributes?.kindref === "compound" && refId?.startsWith("namespace")) result += text; + else result += `\`${text}\``; + } else if (child.tag === "computeroutput") { + // Autolink inline code spans that match known symbols or user-defined literals + const codeText = getAllText(child).trim(); + const target = symbolCatalog?.get(codeText); + if (target) { + result += `[\`${codeText}\`](${target.url})`; + } else { + result += `\`${codeText}\``; + } + } else if (child.tag === "programlisting") { + // Render C++ code snippets with syntax highlighting and line numbers + const codeLines = findChildren(child, "codeline").map((lineNode) => getAllText(lineNode)); + const code = codeLines.length > 0 ? codeLines.join("\n") : getAllText(child); + result += `\n\`\`\`cpp showLineNumbers\n${code.trim()}\n\`\`\`\n\n`; + } else { + result += renderDocNode(child, lang, refMap, symbolCatalog); + } } - if (current) parts.push(current); - return parts; + + return decodeEntities(result); } const anchorLinkIconSvg = ``; -function renderNamespaceContent(publicEnums, publicTypes, publicFuncs, publicVars, labels, lang, refMap = null) { - let md = ""; +function renderMemberCard(anchorId, title, codeBlock, briefDesc, detailedDesc, anchorLabel = "") { + let output = `
\n\n`; + if (anchorId) { + const label = anchorLabel || `Direct link to ${anchorId}`; + output += `
\n`; + output += `${anchorLinkIconSvg}\n\n`; + output += `\`\`\`cpp\n${codeBlock}\n\`\`\`\n\n
\n\n`; + } else if (title) { + output += `### \`${title}\`\n\n\`\`\`cpp\n${codeBlock}\n\`\`\`\n\n`; + } + if (briefDesc) output += `${briefDesc}\n\n`; + if (detailedDesc) output += `${detailedDesc}\n\n`; + output += `
\n\n`; + return output; +} + +function renderNamespaceContent(publicEnums, publicTypes, publicFuncs, publicVars, labels, lang, refMap, symbolCatalog) { + let markdown = ""; if (publicEnums.length > 0) { - md += `## ${labels.publicEnums}\n\n`; - for (const e of publicEnums) { - const eName = cleanSignature(getAllText(findChild(e, "name"))); - const isStrong = e.attributes.strong === "yes"; - const values = findChildren(e, "enumvalue").map((v) => { - const vName = cleanSignature(getAllText(findChild(v, "name"))); - const vInit = cleanSignature(getAllText(findChild(v, "initializer"))); - return ` ${vName}${vInit ? " " + vInit : ""}`; + markdown += `## ${labels.publicEnums}\n\n`; + for (const enumNode of publicEnums) { + const enumName = cleanSignature(getAllText(findChild(enumNode, "name"))); + const isScopedEnum = enumNode.attributes.strong === "yes"; + const enumValues = findChildren(enumNode, "enumvalue").map((valueNode) => { + const valueName = cleanSignature(getAllText(findChild(valueNode, "name"))); + const valueInitializer = cleanSignature(getAllText(findChild(valueNode, "initializer"))); + return ` ${valueName}${valueInitializer ? " " + valueInitializer : ""}`; }); - const eDef = values.length > 0 - ? `enum ${isStrong ? "class " : ""}${eName} {\n${values.join(",\n")}\n};` - : `enum ${isStrong ? "class " : ""}${eName} {};`; - const eBrief = renderDocNode(findChild(e, "briefdescription"), lang, refMap).trim(); - const eDetailed = renderDocNode(findChild(e, "detaileddescription"), lang, refMap).trim(); - md += `### \`${eName}\`\n\n\`\`\`cpp\n${eDef}\n\`\`\`\n\n`; - if (eBrief) md += `${eBrief}\n\n`; - if (eDetailed) md += `${eDetailed}\n\n`; + const enumDefinition = enumValues.length > 0 + ? `enum ${isScopedEnum ? "class " : ""}${enumName} {\n${enumValues.join(",\n")}\n};` + : `enum ${isScopedEnum ? "class " : ""}${enumName} {};`; + const enumBrief = renderDocNode(findChild(enumNode, "briefdescription"), lang, refMap, symbolCatalog).trim(); + const enumDetailed = renderDocNode(findChild(enumNode, "detaileddescription"), lang, refMap, symbolCatalog).trim(); + markdown += renderMemberCard(null, enumName, enumDefinition, enumBrief, enumDetailed); } } if (publicTypes.length > 0) { - md += `## ${labels.publicTypes}\n\n`; - for (const t of publicTypes) { - const tName = cleanSignature(getAllText(findChild(t, "name"))); - const tDef = cleanSignature(getAllText(findChild(t, "definition"))); - const tBrief = renderDocNode(findChild(t, "briefdescription"), lang, refMap).trim(); - md += `### \`${tName}\`\n\n\`\`\`cpp\n${tDef || tName};\n\`\`\`\n\n`; - if (tBrief) md += `${tBrief}\n\n`; + markdown += `## ${labels.publicTypes}\n\n`; + for (const typeNode of publicTypes) { + const typeName = cleanSignature(getAllText(findChild(typeNode, "name"))); + const typeDefinition = cleanTemplateSpaces(cleanSignature(getAllText(findChild(typeNode, "definition")))); + const typeBrief = renderDocNode(findChild(typeNode, "briefdescription"), lang, refMap, symbolCatalog).trim(); + markdown += renderMemberCard(null, typeName, cleanTemplateSpaces(`${typeDefinition || typeName};`), typeBrief, ""); + } + } + + if (publicVars.length > 0) { + markdown += `## ${labels.publicConstants}\n\n`; + const seenVarIds = new Set(); + for (const variableNode of publicVars) { + const variableName = cleanSignature(getAllText(findChild(variableNode, "name"))); + const variableType = cleanCppType(getAllText(findChild(variableNode, "type"))); + const variableInitializer = cleanSignature(getAllText(findChild(variableNode, "initializer"))); + const variableDefinition = cleanTemplateSpaces(cleanSignature(getAllText(findChild(variableNode, "definition")))); + const variableBrief = renderDocNode(findChild(variableNode, "briefdescription"), lang, refMap, symbolCatalog).trim(); + const variableDetailed = renderDocNode(findChild(variableNode, "detaileddescription"), lang, refMap, symbolCatalog).trim(); + + let cleanId = sanitizeHtmlId(variableName) || "var"; + if (seenVarIds.has(cleanId)) { + let overloadCounter = 2; + while (seenVarIds.has(`${cleanId}_${overloadCounter}`)) overloadCounter++; + cleanId = `${cleanId}_${overloadCounter}`; + } + seenVarIds.add(cleanId); + + const anchorLabel = lang === "fr" ? `Lien direct vers ${cleanId}` : `Direct link to ${cleanId}`; + const codeBlock = cleanTemplateSpaces(`${variableDefinition || `${variableType} ${variableName}${variableInitializer ? " " + variableInitializer : ""}`};`); + markdown += renderMemberCard(cleanId, null, codeBlock, variableBrief, variableDetailed, anchorLabel); } } if (publicFuncs.length > 0) { - md += `## ${labels.publicFuncs}\n\n`; + markdown += `## ${labels.publicFuncs}\n\n`; const seenFuncIds = new Set(); - for (const f of publicFuncs) { - const fName = cleanSignature(getAllText(findChild(f, "name"))); - const fTemplate = extractTemplateParams(f); - const fBrief = renderDocNode(findChild(f, "briefdescription"), lang, refMap).trim(); - const fDetailed = renderDocNode(findChild(f, "detaileddescription"), lang, refMap).trim(); - const fSig = formatFunctionSignature(f, ""); - - let cleanId = sanitizeHtmlId(fName) || "func"; + for (const functionNode of publicFuncs) { + const functionName = cleanSignature(getAllText(findChild(functionNode, "name"))); + const functionTemplate = extractTemplateParams(functionNode); + const functionBrief = renderDocNode(findChild(functionNode, "briefdescription"), lang, refMap, symbolCatalog).trim(); + const functionDetailed = renderDocNode(findChild(functionNode, "detaileddescription"), lang, refMap, symbolCatalog).trim(); + const functionSignature = formatFunctionSignature(functionNode, ""); + + let cleanId = sanitizeHtmlId(functionName) || "func"; if (seenFuncIds.has(cleanId)) { - let c = 2; - while (seenFuncIds.has(`${cleanId}_${c}`)) c++; - cleanId = `${cleanId}_${c}`; + let overloadCounter = 2; + while (seenFuncIds.has(`${cleanId}_${overloadCounter}`)) overloadCounter++; + cleanId = `${cleanId}_${overloadCounter}`; } seenFuncIds.add(cleanId); const anchorLabel = lang === "fr" ? `Lien direct vers ${cleanId}` : `Direct link to ${cleanId}`; - md += `
\n${anchorLinkIconSvg}\n\n\`\`\`cpp\n`; - if (fTemplate && !fSig.startsWith("template")) md += `${fTemplate}\n`; - md += `${fSig}\n\`\`\`\n\n
\n\n`; - - if (fBrief) md += `${fBrief}\n\n`; - if (fDetailed) md += `${fDetailed}\n\n`; - md += `---\n\n`; - } - } - - if (publicVars.length > 0) { - md += `## ${labels.publicConstants}\n\n`; - for (const v of publicVars) { - const vName = cleanSignature(getAllText(findChild(v, "name"))); - const vType = cleanSignature(getAllText(findChild(v, "type"))); - const vInit = cleanSignature(getAllText(findChild(v, "initializer"))); - const vDef = cleanSignature(getAllText(findChild(v, "definition"))); - const vBrief = renderDocNode(findChild(v, "briefdescription"), lang, refMap).trim(); - md += `### \`${vName}\`\n\n\`\`\`cpp\n${vDef || `${vType} ${vName}${vInit ? " " + vInit : ""}`};\n\`\`\`\n\n`; - if (vBrief) md += `${vBrief}\n\n`; + const codeBlock = functionTemplate && !functionSignature.startsWith("template") ? `${functionTemplate}\n${functionSignature}` : functionSignature; + markdown += renderMemberCard(cleanId, null, codeBlock, functionBrief, functionDetailed, anchorLabel); } } - return md; + return markdown; } -async function generateForLanguage(lang, compounds, namespaceCompounds = []) { - const outDir = path.join(docsRoot, "src", "content", "docs", lang, "reference"); - - if (fs.existsSync(outDir)) fs.rmSync(outDir, { recursive: true, force: true }); - fs.mkdirSync(outDir, { recursive: true }); - - const isFr = lang === "fr"; - const labels = { - declaration: isFr ? "Déclaration" : "Declaration", - description: isFr ? "Description" : "Description", - publicTypes: isFr ? "Types publics" : "Public types", - publicFuncs: isFr ? "Méthodes et fonctions publiques" : "Public methods & functions", - publicAttrs: isFr ? "Attributs publics" : "Public attributes", - publicEnums: isFr ? "Énumérations publiques" : "Public enums", - publicConstants: isFr ? "Constantes & variables" : "Constants & variables", - namespaceFunctions: isFr ? "Fonctions & utilitaires" : "Functions & utilities", - constants: isFr ? "Constantes" : "Constants", - enums: isFr ? "Énumérations" : "Enums", - types: isFr ? "Définitions de types" : "Type definitions", - namespace: "Namespace:", - fullSymbol: isFr ? "Symbole complet :" : "Full symbol:", - apiRefTitle: isFr ? "Référence d'API CppUtils" : "CppUtils API reference", - apiRefDesc: isFr ? "Catalogue complet des modules et composants de CppUtils" : "Complete catalog of CppUtils modules and components", - }; - +// Index types, classes, structs, and free functions for cross-referencing and autolinking +function buildSymbolCatalog(compounds, namespaceCompounds, lang) { const refMap = new Map(); + const symbolCatalog = new Map(); const slugCounts = new Map(); const primaryTemplates = new Map(); - for (const comp of compounds) { - const rawName = getAllText(findChild(comp, "name")).trim(); + for (const compound of compounds) { + const rawName = getAllText(findChild(compound, "name")).trim(); if (!rawName.startsWith("CppUtils::")) continue; - if (rawName.includes("::detail::") || rawName.includes("::Detail::")) continue; + if (isDetailNamespace(rawName)) continue; - const refId = comp.attributes?.refid; + const refId = compound.attributes?.refid; if (!refId) continue; + const kind = compound.attributes?.kind; const tokens = splitQualifiedName(rawName); const rawSymbolName = tokens.pop(); const symbolName = cleanTemplateSpaces(rawSymbolName); @@ -572,6 +665,23 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { const subNamespace = tokens.join("/"); const namespaceKey = tokens.join("::") || "Core"; + if (kind === "concept") { + const dirPrefix = subNamespace ? `${subNamespace.toLowerCase()}/` : ""; + const url = `/${lang}/reference/${dirPrefix}concepts/#${sanitizeHtmlId(symbolName)}`; + const compoundTarget = { url, name: symbolName }; + refMap.set(refId, compoundTarget); + if (!symbolCatalog.has(symbolName)) symbolCatalog.set(symbolName, compoundTarget); + if (!symbolCatalog.has(rawSymbolName)) symbolCatalog.set(rawSymbolName, compoundTarget); + if (namespaceKey && !symbolCatalog.has(`${namespaceKey}::${symbolName}`)) { + symbolCatalog.set(`${namespaceKey}::${symbolName}`, compoundTarget); + } + const cleanRaw = rawName.replace(/\s+/g, ""); + if (!symbolCatalog.has(cleanRaw)) symbolCatalog.set(cleanRaw, compoundTarget); + const cleanWithoutPrefix = rawName.replace(/^CppUtils::/, "").replace(/\s+/g, ""); + if (!symbolCatalog.has(cleanWithoutPrefix)) symbolCatalog.set(cleanWithoutPrefix, compoundTarget); + continue; + } + let safeBaseName = sanitizeFileName(symbolName); const slugKey = `${subNamespace}/${safeBaseName}`.toLowerCase(); let currentCount = slugCounts.get(slugKey) || 0; @@ -581,8 +691,53 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { const dirPrefix = subNamespace ? `${subNamespace.toLowerCase()}/` : ""; const url = `/${lang}/reference/${dirPrefix}${safeBaseName.toLowerCase()}/`; - refMap.set(refId, { url, name: symbolName }); + const compoundTarget = { url, name: symbolName }; + refMap.set(refId, compoundTarget); + if (!symbolCatalog.has(symbolName)) symbolCatalog.set(symbolName, compoundTarget); + if (!symbolCatalog.has(rawSymbolName)) symbolCatalog.set(rawSymbolName, compoundTarget); + if (namespaceKey && !symbolCatalog.has(`${namespaceKey}::${symbolName}`)) { + symbolCatalog.set(`${namespaceKey}::${symbolName}`, compoundTarget); + } + const cleanRaw = rawName.replace(/\s+/g, ""); + if (!symbolCatalog.has(cleanRaw)) symbolCatalog.set(cleanRaw, compoundTarget); + const cleanWithoutPrefix = rawName.replace(/^CppUtils::/, "").replace(/\s+/g, ""); + if (!symbolCatalog.has(cleanWithoutPrefix)) symbolCatalog.set(cleanWithoutPrefix, compoundTarget); + + // Index class/struct members into refMap and symbolCatalog for autolinking + const seenMemberIds = new Set(); + for (const memberNode of findChildren(compound, "member")) { + const memberRefId = memberNode.attributes?.refid; + const memberName = cleanSignature(getAllText(findChild(memberNode, "name"))); + const memberKind = memberNode.attributes?.kind; + if (!memberRefId || !memberName) continue; + + const isFunction = memberKind === "function"; + let anchor = isFunction ? sanitizeHtmlId(memberName) : sanitizeHtmlId(memberName).toLowerCase(); + if (seenMemberIds.has(anchor)) { + let overloadCounter = 2; + while (seenMemberIds.has(`${anchor}_${overloadCounter}`)) overloadCounter++; + anchor = `${anchor}_${overloadCounter}`; + } + seenMemberIds.add(anchor); + const memberTarget = { url: `${url}#${anchor}`, name: memberName }; + refMap.set(memberRefId, memberTarget); + if (!symbolCatalog.has(`${symbolName}::${memberName}`)) { + symbolCatalog.set(`${symbolName}::${memberName}`, memberTarget); + } + if (!symbolCatalog.has(`${rawSymbolName}::${memberName}`)) { + symbolCatalog.set(`${rawSymbolName}::${memberName}`, memberTarget); + } + if (namespaceKey && !symbolCatalog.has(`${namespaceKey}::${symbolName}::${memberName}`)) { + symbolCatalog.set(`${namespaceKey}::${symbolName}::${memberName}`, memberTarget); + } + const fullMemberSymbol = `CppUtils::${tokens.concat([symbolName, memberName]).join("::")}`; + if (!symbolCatalog.has(fullMemberSymbol)) { + symbolCatalog.set(fullMemberSymbol, memberTarget); + } + } + + // Index primary template signatures to cross-reference partial specializations if (!symbolName.includes("<")) { let primaryTemplateParams = ""; const xmlPath = path.join(xmlDir, `${refId}.xml`); @@ -593,10 +748,10 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { const paramTags = match[1].match(/[\s\S]*?<\/param>/g) || []; const params = paramTags.map((p) => { const type = cleanSignature((p.match(/([\s\S]*?)<\/type>/)?.[1] || "").replace(/<[^>]+>/g, "")); - const declname = cleanSignature((p.match(/([\s\S]*?)<\/declname>/)?.[1] || "").replace(/<[^>]+>/g, "")); - const defval = cleanSignature((p.match(/([\s\S]*?)<\/defval>/)?.[1] || "").replace(/<[^>]+>/g, "")); - let str = [type, declname].filter(Boolean).join(" "); - if (defval) str += ` = ${defval}`; + const declarationName = cleanSignature((p.match(/([\s\S]*?)<\/declname>/)?.[1] || "").replace(/<[^>]+>/g, "")); + const defaultValue = cleanSignature((p.match(/([\s\S]*?)<\/defval>/)?.[1] || "").replace(/<[^>]+>/g, "")); + let str = [type, declarationName].filter(Boolean).join(" "); + if (defaultValue) str += ` = ${defaultValue}`; return str; }); if (params.length > 0) primaryTemplateParams = `<${params.join(", ")}>`; @@ -610,17 +765,135 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { } } + // Index free functions and namespaces into symbolCatalog and refMap + for (const namespaceCompound of namespaceCompounds) { + const rawName = getAllText(findChild(namespaceCompound, "name")).trim(); + if (!rawName.startsWith("CppUtils")) continue; + if (isDetailNamespace(rawName)) continue; + + const tokens = splitQualifiedName(rawName); + if (tokens[0] === "CppUtils") tokens.shift(); + const subNamespace = tokens.join("/"); + const namespaceKey = tokens.join("::") || "Core"; + const dirPrefix = subNamespace ? `${subNamespace.toLowerCase()}/` : ""; + const url = subNamespace ? `/${lang}/reference/${dirPrefix}functions/` : `/${lang}/reference/globalfunctions/`; + + // Index the namespace compound so cross-references to qualified sub-namespaces resolve to their functions page + const namespaceRefId = namespaceCompound.attributes?.refid; + if (namespaceRefId) { + const namespaceTarget = { url, name: namespaceKey }; + refMap.set(namespaceRefId, namespaceTarget); + if (!symbolCatalog.has(namespaceKey)) { + symbolCatalog.set(namespaceKey, namespaceTarget); + } + const fullNamespaceSymbol = `CppUtils::${namespaceKey}`; + if (!symbolCatalog.has(fullNamespaceSymbol)) { + symbolCatalog.set(fullNamespaceSymbol, namespaceTarget); + } + } + + for (const memberNode of findChildren(namespaceCompound, "member")) { + const memberRefId = memberNode.attributes?.refid; + const memberName = cleanSignature(getAllText(findChild(memberNode, "name"))); + const memberKind = memberNode.attributes?.kind; + if (!memberRefId || !memberName) continue; + if (memberKind !== "function" && memberKind !== "enum" && memberKind !== "typedef" && memberKind !== "variable") continue; + + // Functions and variables use HTML divs with preserved casing in id. + // Enums and types use markdown headings ### `Name` which Starlight lowercases in the anchor. + const isMarkdownHeading = memberKind === "enum" || memberKind === "typedef"; + const anchor = isMarkdownHeading ? sanitizeHtmlId(memberName).toLowerCase() : sanitizeHtmlId(memberName); + const memberTarget = { url: `${url}#${anchor}`, name: memberName }; + refMap.set(memberRefId, memberTarget); + if (!symbolCatalog.has(memberName)) symbolCatalog.set(memberName, memberTarget); + if (namespaceKey && !symbolCatalog.has(`${namespaceKey}::${memberName}`)) { + symbolCatalog.set(`${namespaceKey}::${memberName}`, memberTarget); + } + const fullMemberSymbol = `CppUtils::${tokens.concat([memberName]).join("::")}`; + if (!symbolCatalog.has(fullMemberSymbol)) { + symbolCatalog.set(fullMemberSymbol, memberTarget); + } + + // Index user-defined literal suffixes without the operator prefix for direct symbol resolution + if (memberName.startsWith('operator""')) { + const literalSuffix = memberName.slice(10).trim(); + if (literalSuffix) { + if (!symbolCatalog.has(literalSuffix) || rawName.includes("Literals")) { + symbolCatalog.set(literalSuffix, memberTarget); + } + if (namespaceKey && !symbolCatalog.has(`${namespaceKey}::${literalSuffix}`)) { + symbolCatalog.set(`${namespaceKey}::${literalSuffix}`, memberTarget); + } + } + } + } + } + + return { refMap, symbolCatalog, primaryTemplates }; +} + +function getBadgeInfo(kind) { + switch (kind?.toLowerCase()) { + case "class": + return { badgeText: "CLASS", badgeClass: "ref-badge ref-badge-class" }; + case "struct": + return { badgeText: "STRUCT", badgeClass: "ref-badge ref-badge-struct" }; + case "concept": + case "concepts": + return { badgeText: "CONCEPTS", badgeClass: "ref-badge ref-badge-concept" }; + case "functions": + return { badgeText: "FUNCTIONS", badgeClass: "ref-badge ref-badge-functions" }; + case "enum": + return { badgeText: "ENUM", badgeClass: "ref-badge ref-badge-enum" }; + case "constants": + return { badgeText: "CONST", badgeClass: "ref-badge ref-badge-constants" }; + case "types": + return { badgeText: "TYPE", badgeClass: "ref-badge ref-badge-types" }; + default: + return { badgeText: (kind || "").toUpperCase(), badgeClass: "ref-badge" }; + } +} + +async function generateForLanguage(lang, compounds, namespaceCompounds = []) { + const outDir = path.join(docsRoot, "src", "content", "docs", lang, "reference"); + if (fs.existsSync(outDir)) fs.rmSync(outDir, { recursive: true, force: true }); + fs.mkdirSync(outDir, { recursive: true }); + + const isFr = lang === "fr"; + const labels = { + declaration: isFr ? "Déclaration" : "Declaration", + description: isFr ? "Description" : "Description", + publicTypes: isFr ? "Types publics" : "Public types", + publicMethods: isFr ? "Méthodes publiques" : "Public methods", + publicFuncs: isFr ? "Fonctions publiques" : "Public functions", + publicAttrs: isFr ? "Attributs publics" : "Public attributes", + publicEnums: isFr ? "Énumérations publiques" : "Public enums", + publicConstants: isFr ? "Constantes & variables" : "Constants & variables", + namespaceFunctions: isFr ? "Fonctions & utilitaires" : "Functions & utilities", + constants: isFr ? "Constantes" : "Constants", + enums: isFr ? "Énumérations" : "Enums", + types: isFr ? "Définitions de types" : "Type definitions", + namespace: "Namespace:", + fullSymbol: isFr ? "Symbole complet :" : "Full symbol:", + apiRefTitle: isFr ? "Référence d'API CppUtils" : "CppUtils API reference", + apiRefDesc: isFr ? "Catalogue complet des modules et composants de CppUtils" : "Complete catalog of CppUtils modules and components", + publicConcepts: isFr ? "Concepts publics" : "Public concepts", + concepts: "Concepts", + }; + + const { refMap, symbolCatalog, primaryTemplates } = buildSymbolCatalog(compounds, namespaceCompounds, lang); const namespacesMap = new Map(); + const conceptsByNamespace = new Map(); const usedSlugs = new Set(); let generatedCount = 0; - for (const comp of compounds) { - const kind = comp.attributes.kind; - const rawName = getAllText(findChild(comp, "name")).trim(); + for (const compound of compounds) { + const kind = compound.attributes.kind; + const rawName = getAllText(findChild(compound, "name")).trim(); if (!rawName.startsWith("CppUtils::")) continue; - if (rawName.includes("::detail::") || rawName.includes("::Detail::")) continue; + if (isDetailNamespace(rawName)) continue; - const refId = comp.attributes.refid; + const refId = compound.attributes.refid; const xmlPath = path.join(xmlDir, `${refId}.xml`); if (!fs.existsSync(xmlPath)) continue; @@ -629,6 +902,8 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { const compounddef = findChild(fileTree, "compounddef"); if (!compounddef) continue; + const compoundFile = findChild(compounddef, "location")?.attributes?.file; + const tokens = splitQualifiedName(rawName); const rawSymbolName = tokens.pop(); const symbolName = cleanTemplateSpaces(rawSymbolName); @@ -636,23 +911,49 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { const subNamespace = tokens.join("/"); const namespaceKey = tokens.join("::") || "Core"; + if (kind === "concept") { + if (!conceptsByNamespace.has(namespaceKey)) { + conceptsByNamespace.set(namespaceKey, { + tokens: [...tokens], + subNamespace, + fullNamespaceName: `CppUtils${tokens.length > 0 ? "::" + tokens.join("::") : ""}`, + concepts: [], + }); + } + const briefDesc = renderDocNode(findChild(compounddef, "briefdescription"), lang, refMap, symbolCatalog).trim(); + const detailedDesc = renderDocNode(findChild(compounddef, "detaileddescription"), lang, refMap, symbolCatalog).trim(); + conceptsByNamespace.get(namespaceKey).concepts.push({ + compounddef, + symbolName, + rawName, + briefDesc, + detailedDesc, + compoundFile, + }); + if (lang === "en") { + const cleanFullName = rawName.replace(/\s+/g, ""); + cpputilsSymbols[cleanFullName] = `/reference/${subNamespace ? `${subNamespace.toLowerCase()}/` : ""}concepts/#${sanitizeHtmlId(symbolName)}`; + } + continue; + } + const targetDir = subNamespace ? path.join(outDir, subNamespace) : outDir; fs.mkdirSync(targetDir, { recursive: true }); let safeBaseName = sanitizeFileName(symbolName); const slugKey = `${subNamespace}/${safeBaseName}`.toLowerCase(); if (usedSlugs.has(slugKey)) { - let counter = 2; - while (usedSlugs.has(`${slugKey}_${counter}`)) counter++; - safeBaseName = `${safeBaseName}_${counter}`; - usedSlugs.add(`${slugKey}_${counter}`); + let overloadCounter = 2; + while (usedSlugs.has(`${slugKey}_${overloadCounter}`)) overloadCounter++; + safeBaseName = `${safeBaseName}_${overloadCounter}`; + usedSlugs.add(`${slugKey}_${overloadCounter}`); } else usedSlugs.add(slugKey); const fileName = `${safeBaseName}.mdx`; const filePath = path.join(targetDir, fileName); - const briefDesc = renderDocNode(findChild(compounddef, "briefdescription"), lang, refMap).trim(); - const detailedDesc = renderDocNode(findChild(compounddef, "detaileddescription"), lang, refMap).trim(); + const briefDesc = renderDocNode(findChild(compounddef, "briefdescription"), lang, refMap, symbolCatalog).trim(); + const detailedDesc = renderDocNode(findChild(compounddef, "detaileddescription"), lang, refMap, symbolCatalog).trim(); const templateHeader = extractTemplateParams(compounddef); const pageDesc = isFr @@ -660,17 +961,27 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { : `API Reference for CppUtils::${tokens.concat([symbolName]).join("::")}`; const nsPrefix = `CppUtils::${tokens.length > 0 ? tokens.join("::") + "::" : ""}`; - - const compLoc = findChild(compounddef, "location"); - const compFile = compLoc?.attributes?.file; - const moduleName = extractModuleFromSource(compFile) || (tokens.length > 0 ? `CppUtils.${tokens.join(".")}` : "CppUtils"); - + // Extract C++20 module name if declared in the source file + const moduleName = extractModuleFromSource(compoundFile) || (tokens.length > 0 ? `CppUtils.${tokens.join(".")}` : "CppUtils"); const templateBrackets = templateHeader ? templateHeader.replace(/^\s*template\s*/, "").trim() : ""; + // Detect partial or full template specializations (e.g. MyType) const specMatch = symbolName.match(/^([^<]+)(<.+>)$/); const baseSymbolName = specMatch ? specMatch[1].trim() : symbolName; const specializationArgs = specMatch ? specMatch[2].trim() : ""; + const symbolSlug = `/reference/${subNamespace ? `${subNamespace}/` : ""}${safeBaseName}/`.toLowerCase(); + if (lang === "en") { + const cleanFullName = rawName.replace(/\s+/g, ""); + cpputilsSymbols[cleanFullName] = symbolSlug; + if (baseSymbolName !== symbolName) { + const baseFullName = `CppUtils::${tokens.concat([baseSymbolName]).join("::")}`; + if (!cpputilsSymbols[baseFullName]) { + cpputilsSymbols[baseFullName] = symbolSlug; + } + } + } + const displayTitle = baseSymbolName; const displayTemplate = specializationArgs || templateBrackets; @@ -696,84 +1007,98 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { if (briefDesc) markdown += `${briefDesc}\n\n`; - if (kind === "concept") { - const initNode = findChild(compounddef, "initializer"); - const initText = cleanSignature(getAllText(initNode)); - markdown += `## ${labels.declaration}\n\n\`\`\`cpp\n`; - if (templateHeader) markdown += `${templateHeader}\n`; - markdown += `${initText || `concept ${symbolName} = ...;`}\n\`\`\`\n\n`; + if (detailedDesc) markdown += `## ${labels.description}\n\n${detailedDesc}\n\n`; + + // Collect public members grouped by category (methods, types, attributes) + const sections = findChildren(compounddef, "sectiondef"); + const publicFuncs = []; + const publicTypes = []; + const publicAttrs = []; + + for (const section of sections) { + const sectionKind = section.attributes.kind; + const members = findChildren(section, "memberdef"); + for (const memberNode of members) { + const prot = memberNode.attributes.prot; + if (prot !== "public") continue; + + if (sectionKind.includes("func")) publicFuncs.push(memberNode); + else if (sectionKind.includes("type")) publicTypes.push(memberNode); + else if (sectionKind.includes("attrib")) publicAttrs.push(memberNode); + } } - if (detailedDesc) markdown += `## ${labels.description}\n\n${detailedDesc}\n\n`; + const seenMemberIds = new Set(); - if (kind !== "concept") { - const sections = findChildren(compounddef, "sectiondef"); - const publicFuncs = []; - const publicTypes = []; - const publicAttrs = []; - - for (const sec of sections) { - const secKind = sec.attributes.kind; - const members = findChildren(sec, "memberdef"); - for (const m of members) { - const prot = m.attributes.prot; - if (prot !== "public") continue; - - if (secKind.includes("func")) publicFuncs.push(m); - else if (secKind.includes("type")) publicTypes.push(m); - else if (secKind.includes("attrib")) publicAttrs.push(m); - } + if (publicTypes.length > 0) { + markdown += `## ${labels.publicTypes}\n\n`; + for (const typeNode of publicTypes) { + const typeName = cleanSignature(getAllText(findChild(typeNode, "name"))); + const typeDefinition = cleanTemplateSpaces(cleanSignature(getAllText(findChild(typeNode, "definition")))); + const typeBrief = renderDocNode(findChild(typeNode, "briefdescription"), lang, refMap, symbolCatalog).trim(); + markdown += renderMemberCard(null, typeName, cleanTemplateSpaces(`${typeDefinition || typeName};`), typeBrief, ""); } + } - if (publicTypes.length > 0) { - markdown += `## ${labels.publicTypes}\n\n`; - for (const t of publicTypes) { - const tName = cleanSignature(getAllText(findChild(t, "name"))); - const tDef = cleanSignature(getAllText(findChild(t, "definition"))); - const tBrief = renderDocNode(findChild(t, "briefdescription"), lang, refMap).trim(); - markdown += `### \`${tName}\`\n\n\`\`\`cpp\n${tDef || tName};\n\`\`\`\n\n`; - if (tBrief) markdown += `${tBrief}\n\n`; + if (publicAttrs.length > 0) { + markdown += `## ${labels.publicAttrs}\n\n`; + for (const attributeNode of publicAttrs) { + const attributeName = cleanSignature(getAllText(findChild(attributeNode, "name"))); + const attributeType = cleanCppType(getAllText(findChild(attributeNode, "type"))); + const attributeInitializer = cleanSignature(getAllText(findChild(attributeNode, "initializer"))); + const attributeBrief = renderDocNode(findChild(attributeNode, "briefdescription"), lang, refMap, symbolCatalog).trim(); + const attributeDetailed = renderDocNode(findChild(attributeNode, "detaileddescription"), lang, refMap, symbolCatalog).trim(); + + let isStatic = attributeNode.attributes?.static === "yes"; + let isConstexpr = attributeNode.attributes?.constexpr === "yes"; + let prefix = ""; + if (isStatic) prefix += "static "; + if (isConstexpr) prefix += "constexpr "; + + let cleanId = sanitizeHtmlId(attributeName) || "attr"; + if (seenMemberIds.has(cleanId)) { + let counter = 2; + while (seenMemberIds.has(`${cleanId}_${counter}`)) counter++; + cleanId = `${cleanId}_${counter}`; } - } + seenMemberIds.add(cleanId); - if (publicFuncs.length > 0) { - markdown += `## ${labels.publicFuncs}\n\n`; - const seenFuncIds = new Set(); - for (const f of publicFuncs) { - const fName = cleanSignature(getAllText(findChild(f, "name"))); - const fTemplate = extractTemplateParams(f); - const fBrief = renderDocNode(findChild(f, "briefdescription"), lang, refMap).trim(); - const fDetailed = renderDocNode(findChild(f, "detaileddescription"), lang, refMap).trim(); - const fSig = formatFunctionSignature(f, baseSymbolName); - - let cleanId = sanitizeHtmlId(fName) || "method"; - if (seenFuncIds.has(cleanId)) { - let c = 2; - while (seenFuncIds.has(`${cleanId}_${c}`)) c++; - cleanId = `${cleanId}_${c}`; - } - seenFuncIds.add(cleanId); + if (lang === "en") { + const cleanFullName = rawName.replace(/\s+/g, ""); + cpputilsSymbols[`${cleanFullName}::${attributeName}`] = `${symbolSlug}#${cleanId}`; + } - const anchorLabel = lang === "fr" ? `Lien direct vers ${cleanId}` : `Direct link to ${cleanId}`; - markdown += `
\n${anchorLinkIconSvg}\n\n\`\`\`cpp\n`; - if (fTemplate && !fSig.startsWith("template")) markdown += `${fTemplate}\n`; - markdown += `${fSig}\n\`\`\`\n\n
\n\n`; + const anchorLabel = lang === "fr" ? `Lien direct vers ${cleanId}` : `Direct link to ${cleanId}`; + const codeBlock = cleanTemplateSpaces(`${prefix}${attributeType} ${attributeName}${attributeInitializer ? " " + attributeInitializer : ""};`); + markdown += renderMemberCard(cleanId, null, codeBlock, attributeBrief, attributeDetailed, anchorLabel); + } + } - if (fBrief) markdown += `${fBrief}\n\n`; - if (fDetailed) markdown += `${fDetailed}\n\n`; - markdown += `---\n\n`; + if (publicFuncs.length > 0) { + markdown += `## ${labels.publicMethods}\n\n`; + for (const functionNode of publicFuncs) { + const functionName = cleanSignature(getAllText(findChild(functionNode, "name"))); + const functionTemplate = extractTemplateParams(functionNode); + const functionBrief = renderDocNode(findChild(functionNode, "briefdescription"), lang, refMap, symbolCatalog).trim(); + const functionDetailed = renderDocNode(findChild(functionNode, "detaileddescription"), lang, refMap, symbolCatalog).trim(); + const functionSignature = formatFunctionSignature(functionNode, baseSymbolName); + + let cleanId = sanitizeHtmlId(functionName) || "method"; + if (seenMemberIds.has(cleanId)) { + let overloadCounter = 2; + while (seenMemberIds.has(`${cleanId}_${overloadCounter}`)) overloadCounter++; + cleanId = `${cleanId}_${overloadCounter}`; } - } + seenMemberIds.add(cleanId); - if (publicAttrs.length > 0) { - markdown += `## ${labels.publicAttrs}\n\n`; - for (const a of publicAttrs) { - const aName = cleanSignature(getAllText(findChild(a, "name"))); - const aType = cleanSignature(getAllText(findChild(a, "type"))); - const aBrief = renderDocNode(findChild(a, "briefdescription"), lang, refMap).trim(); - markdown += `### \`${aName}\`\n\n\`\`\`cpp\n${aType} ${aName};\n\`\`\`\n\n`; - if (aBrief) markdown += `${aBrief}\n\n`; + if (lang === "en") { + const cleanFullName = rawName.replace(/\s+/g, ""); + cpputilsSymbols[`${cleanFullName}::${functionName}`] = `${symbolSlug}#${cleanId}`; } + + const anchorLabel = lang === "fr" ? `Lien direct vers ${cleanId}` : `Direct link to ${cleanId}`; + const codeBlock = functionTemplate && !functionSignature.startsWith("template") ? `${functionTemplate}\n${functionSignature}` : functionSignature; + markdown += renderMemberCard(cleanId, null, codeBlock, functionBrief, functionDetailed, anchorLabel); } } @@ -784,13 +1109,50 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { namespacesMap.get(namespaceKey).push({ name: symbolName, kind, file: fileName, subNamespace }); } + // Generate Concepts.mdx for each namespace containing exported concepts + for (const [namespaceKey, data] of conceptsByNamespace) { + const { tokens, subNamespace, fullNamespaceName, concepts } = data; + if (concepts.length === 0) continue; + + concepts.sort((a, b) => a.symbolName.localeCompare(b.symbolName)); + + const targetDir = subNamespace ? path.join(outDir, subNamespace) : outDir; + fs.mkdirSync(targetDir, { recursive: true }); + const filePath = path.join(targetDir, "Concepts.mdx"); + + const title = labels.concepts; + const pageDesc = isFr + ? `Concepts publics du namespace ${fullNamespaceName}` + : `Public concepts for namespace ${fullNamespaceName}`; + + const firstFile = concepts[0]?.compoundFile; + const moduleName = extractModuleFromSource(firstFile) || (tokens.length > 0 ? `CppUtils.${tokens.join(".")}` : "CppUtils"); + + let markdown = `---\ntitle: "${title}"\ndescription: "${pageDesc}"\nbadge: "CONCEPTS"\nmoduleImport: "${moduleName}"\nsidebar:\n order: 2\n---\n\n`; + markdown += `**${labels.namespace}** \`${fullNamespaceName}\`\n\n`; + markdown += `## ${labels.publicConcepts}\n\n`; + + for (const concept of concepts) { + const anchorId = sanitizeHtmlId(concept.symbolName); + const anchorLabel = isFr ? `Lien direct vers ${concept.symbolName}` : `Direct link to ${concept.symbolName}`; + const codeBlock = formatConceptDeclaration(concept.compounddef, concept.symbolName); + markdown += renderMemberCard(anchorId, null, codeBlock, concept.briefDesc, concept.detailedDesc, anchorLabel); + } + + fs.writeFileSync(filePath, markdown, "utf-8"); + generatedCount++; + + if (!namespacesMap.has(namespaceKey)) namespacesMap.set(namespaceKey, []); + namespacesMap.get(namespaceKey).push({ name: labels.concepts, kind: "concepts", file: "Concepts.mdx", subNamespace }); + } + // Process namespace-level compounds (free functions, enums, constants, typedefs) - for (const nsComp of namespaceCompounds || []) { - const rawName = getAllText(findChild(nsComp, "name")).trim(); + for (const namespaceCompound of namespaceCompounds || []) { + const rawName = getAllText(findChild(namespaceCompound, "name")).trim(); if (!rawName.startsWith("CppUtils")) continue; - if (rawName.includes("::detail") || rawName.includes("::Detail")) continue; + if (isDetailNamespace(rawName)) continue; - const refId = nsComp.attributes.refid; + const refId = namespaceCompound.attributes.refid; const xmlPath = path.join(xmlDir, `${refId}.xml`); if (!fs.existsSync(xmlPath)) continue; @@ -806,20 +1168,20 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { const publicVars = []; const seenIds = new Set(); - for (const sec of sections) { - const secKind = sec.attributes.kind; - const members = findChildren(sec, "memberdef"); - for (const m of members) { - const prot = m.attributes.prot || "public"; + for (const section of sections) { + const sectionKind = section.attributes.kind; + const members = findChildren(section, "memberdef"); + for (const memberNode of members) { + const prot = memberNode.attributes.prot || "public"; if (prot !== "public") continue; - const mId = m.attributes.id; - if (seenIds.has(mId)) continue; - seenIds.add(mId); - - if (secKind.includes("func")) publicFuncs.push(m); - else if (secKind === "enum") publicEnums.push(m); - else if (secKind === "typedef") publicTypes.push(m); - else if (secKind === "var") publicVars.push(m); + const memberId = memberNode.attributes.id; + if (seenIds.has(memberId)) continue; + seenIds.add(memberId); + + if (sectionKind.includes("func")) publicFuncs.push(memberNode); + else if (sectionKind.includes("enum")) publicEnums.push(memberNode); + else if (sectionKind.includes("typedef")) publicTypes.push(memberNode); + else if (sectionKind.includes("var")) publicVars.push(memberNode); } } @@ -834,91 +1196,110 @@ async function generateForLanguage(lang, compounds, namespaceCompounds = []) { let title = labels.namespaceFunctions; let badgeText = "FUNCTIONS"; let itemKind = "functions"; + let sidebarOrder = 1; if (publicFuncs.length > 0) { fileName = "Functions.mdx"; title = labels.namespaceFunctions; badgeText = "FUNCTIONS"; itemKind = "functions"; - } else if (publicEnums.length > 0) { - fileName = "Enums.mdx"; - title = labels.enums; - badgeText = "ENUM"; - itemKind = "enum"; + sidebarOrder = 1; } else if (publicVars.length > 0) { fileName = "Constants.mdx"; title = labels.constants; badgeText = "CONSTANTS"; itemKind = "constants"; + sidebarOrder = 3; + } else if (publicEnums.length > 0) { + fileName = "Enums.mdx"; + title = labels.enums; + badgeText = "ENUM"; + itemKind = "enum"; + sidebarOrder = 4; } else { fileName = "Types.mdx"; title = labels.types; badgeText = "TYPES"; itemKind = "types"; + sidebarOrder = 5; } if (!subNamespace) { fileName = "GlobalFunctions.mdx"; title = isFr ? "Fonctions globales CppUtils" : "Global CppUtils Functions"; + sidebarOrder = 1; } const targetDir = subNamespace ? path.join(outDir, subNamespace) : outDir; fs.mkdirSync(targetDir, { recursive: true }); const filePath = path.join(targetDir, fileName); - const fullNsName = rawName; + const fullNamespaceName = rawName; const pageDesc = isFr - ? `${title} du namespace ${fullNsName}` - : `${title} for namespace ${fullNsName}`; - - const nsModule = fullNsName.replace(/::/g, "."); - let markdown = `---\ntitle: "${title}"\ndescription: "${pageDesc}"\nbadge: "${badgeText}"\nmoduleImport: "${nsModule}"\n---\n\n`; - markdown += `**${labels.namespace}** \`${fullNsName}\`\n\n`; - - markdown += renderNamespaceContent(publicEnums, publicTypes, publicFuncs, publicVars, labels, lang, refMap); + ? `${title} du namespace ${fullNamespaceName}` + : `${title} for namespace ${fullNamespaceName}`; + + const nsModule = fullNamespaceName.replace(/::/g, "."); + const sidebarSection = tokens.length <= 1 ? `sidebar:\n order: ${sidebarOrder}\n` : ""; + let markdown = `---\ntitle: "${title}"\ndescription: "${pageDesc}"\nbadge: "${badgeText}"\nmoduleImport: "${nsModule}"\n${sidebarSection}---\n\n`; + markdown += `**${labels.namespace}** \`${fullNamespaceName}\`\n\n`; + const nsBrief = renderDocNode(findChild(compounddef, "briefdescription"), lang, refMap, symbolCatalog).trim(); + const nsDetailed = renderDocNode(findChild(compounddef, "detaileddescription"), lang, refMap, symbolCatalog).trim(); + if (nsBrief) markdown += `${nsBrief}\n\n`; + if (nsDetailed) markdown += `${nsDetailed}\n\n`; + markdown += renderNamespaceContent(publicEnums, publicTypes, publicFuncs, publicVars, labels, lang, refMap, symbolCatalog); fs.writeFileSync(filePath, markdown, "utf-8"); generatedCount++; + if (lang === "en") { + const baseNamespaceSlug = `/reference/${subNamespace ? `${subNamespace.toLowerCase()}/` : ""}${fileName.replace(/\.mdx$/, "").toLowerCase()}/`; + for (const functionNode of publicFuncs) { + const functionName = cleanSignature(getAllText(findChild(functionNode, "name"))); + const fullSymbol = `${fullNamespaceName}::${functionName}`; + if (!cpputilsSymbols[fullSymbol]) { + cpputilsSymbols[fullSymbol] = `${baseNamespaceSlug}#${sanitizeHtmlId(functionName)}`; + } + } + for (const enumNode of publicEnums) { + const enumName = cleanSignature(getAllText(findChild(enumNode, "name"))); + const fullSymbol = `${fullNamespaceName}::${enumName}`; + if (!cpputilsSymbols[fullSymbol]) { + cpputilsSymbols[fullSymbol] = `${baseNamespaceSlug}#${sanitizeHtmlId(enumName).toLowerCase()}`; + } + } + for (const variableNode of publicVars) { + const variableName = cleanSignature(getAllText(findChild(variableNode, "name"))); + const fullSymbol = `${fullNamespaceName}::${variableName}`; + if (!cpputilsSymbols[fullSymbol]) { + cpputilsSymbols[fullSymbol] = `${baseNamespaceSlug}#${sanitizeHtmlId(variableName)}`; + } + } + for (const typeNode of publicTypes) { + const typeName = cleanSignature(getAllText(findChild(typeNode, "name"))); + const fullSymbol = `${fullNamespaceName}::${typeName}`; + if (!cpputilsSymbols[fullSymbol]) { + cpputilsSymbols[fullSymbol] = `${baseNamespaceSlug}#${sanitizeHtmlId(typeName).toLowerCase()}`; + } + } + } + if (!namespacesMap.has(namespaceKey)) namespacesMap.set(namespaceKey, []); namespacesMap.get(namespaceKey).push({ name: title, kind: itemKind, file: fileName, subNamespace }); } -function getBadgeInfo(kind) { - switch (kind?.toLowerCase()) { - case "class": - return { badgeText: "CLASS", badgeClass: "ref-badge ref-badge-class" }; - case "struct": - return { badgeText: "STRUCT", badgeClass: "ref-badge ref-badge-struct" }; - case "concept": - return { badgeText: "CONCEPT", badgeClass: "ref-badge ref-badge-concept" }; - case "functions": - return { badgeText: "FUNCTIONS", badgeClass: "ref-badge ref-badge-functions" }; - case "enum": - return { badgeText: "ENUM", badgeClass: "ref-badge ref-badge-enum" }; - case "constants": - return { badgeText: "CONST", badgeClass: "ref-badge ref-badge-constants" }; - case "types": - return { badgeText: "TYPE", badgeClass: "ref-badge ref-badge-types" }; - default: - return { badgeText: (kind || "").toUpperCase(), badgeClass: "ref-badge" }; - } -} - // Create Reference Overview Index page let indexMd = `---\ntitle: "${labels.apiRefTitle}"\ndescription: "${labels.apiRefDesc}"\n---\n\n`; indexMd += `export const base = import.meta.env.BASE_URL.replace(/\\/$/, '');\n\n`; const sortedNamespaces = [...namespacesMap.keys()].sort(); - indexMd += `
\n`; indexMd += `
\n`; - for (const ns of sortedNamespaces) { - const items = namespacesMap.get(ns).sort((a, b) => a.name.localeCompare(b.name)); - + for (const namespaceName of sortedNamespaces) { + const items = namespacesMap.get(namespaceName).sort((a, b) => a.name.localeCompare(b.name)); indexMd += `
\n`; indexMd += `
\n`; - indexMd += `

CppUtils::${ns}

\n`; + indexMd += `

CppUtils::${namespaceName}

\n`; indexMd += `
\n`; indexMd += `
    \n`; for (const item of items) { @@ -940,7 +1321,6 @@ function getBadgeInfo(kind) { indexMd += `
\n`; fs.writeFileSync(path.join(outDir, "index.mdx"), indexMd, "utf-8"); - console.log(`[generate-api] [${lang.toUpperCase()}] Successfully generated ${generatedCount} API reference pages across ${sortedNamespaces.length} namespaces.`); } @@ -959,6 +1339,10 @@ async function main() { // Generate symmetrically for English and French await generateForLanguage("en", targetCompounds, namespaceCompounds); await generateForLanguage("fr", targetCompounds, namespaceCompounds); + + const symbolsOutPath = path.join(docsRoot, "src", "data", "cpputils-symbols.json"); + fs.writeFileSync(symbolsOutPath, JSON.stringify(cpputilsSymbols, null, 2), "utf-8"); + console.log(`[generate-api] Saved ${Object.keys(cpputilsSymbols).length} CppUtils symbols to ${symbolsOutPath}`); } main().catch((err) => { diff --git a/docs/src/components/TableOfContentsProgress.astro b/docs/src/components/TableOfContentsProgress.astro index 4f30f0eb..bdcb0262 100644 --- a/docs/src/components/TableOfContentsProgress.astro +++ b/docs/src/components/TableOfContentsProgress.astro @@ -1,53 +1,149 @@ --- + --- diff --git a/docs/src/content/docs/en/guides/lockers.mdx b/docs/src/content/docs/en/guides/lockers.mdx index e215410f..736809dd 100644 --- a/docs/src/content/docs/en/guides/lockers.mdx +++ b/docs/src/content/docs/en/guides/lockers.mdx @@ -1,8 +1,8 @@ --- -title: RAII synchronization (Lockers) +title: RAII Lockers description: Comprehensive guide to multithreaded synchronization, encapsulation, pitfalls, and best practices in CppUtils::Thread. sidebar: - order: 3 + order: 4 --- import { Card, CardGrid, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; @@ -208,6 +208,57 @@ auto account = CppUtils::Thread::UniqueLocker{}; } // Scope exit: single lock release, zero broken atomicity window ``` +### Member lockers and the mutable keyword: logical vs physical constness + +When storing a `UniqueLocker` or `SharedLocker` as a member variable inside a class, a fundamental architectural design decision arises: should this member be declared `mutable`? + +The answer is **yes, in the vast majority of concurrent architectures**, due to the essential distinction between logical constness and physical (bitwise) constness: + +- **Logical constness**: An observation method (such as `count() const`, `snapshot() const`, or `hasOccurred() const`) promises callers that it does not alter the observable abstract state of the host object. +- **Physical (bitwise) constness**: Acquiring a lock physically modifies an internal atomic variable or flag inside the mutex. + +#### Concrete use case: Synchronization and condition variables inside const methods + +Consider an event monitoring utility such as `EventWatcher`. Its inspection and waiting methods are naturally `const` from the caller's perspective: + +```cpp showLineNumbers +class EventWatcher +{ + struct State + { + std::size_t count = 0; + std::vector payloads; + }; + + // The mutable keyword allows locking and condition waits within const methods + mutable CppUtils::Thread::UniqueLocker m_state; + std::condition_variable m_condition; + +public: + [[nodiscard]] auto count() const noexcept -> std::size_t + { + return m_state.access()->count; + } + + auto waitForOccurrences(std::size_t target, std::chrono::milliseconds timeout) const -> bool + { + auto accessor = m_state.access(); + // std::condition_variable::wait_for requires an active, modifiable std::unique_lock + return m_condition.wait_for(accessor.getLockGuard(), timeout, [&] { + return accessor->count >= target; + }); + } +}; +``` + +Without the `mutable` specifier on `m_state`: +1. In a `const` member function, the compiler treats `m_state` as `const UniqueLocker`. +2. Invoking `m_state.access()` calls the `const&` overload, returning an `Accessor`. +3. Synchronization primitives that atomically release and re-acquire locks (such as `condition_variable::wait_for` requiring `accessor.getLockGuard()`) fail to compile because the underlying lock cannot be modified. +4. Lazy initialization, memoization, or thread-safe caching under lock inside a `const` method would be impossible without breaking encapsulation. + +Declaring member lockers as `mutable` ensures that operational thread-safety mechanics remain strictly decoupled from the logical constness guarantees of your public APIs. + --- ## Shared access with SharedLocker diff --git a/docs/src/content/docs/en/guides/unit-test.mdx b/docs/src/content/docs/en/guides/unit-test.mdx new file mode 100644 index 00000000..ec4ef6d3 --- /dev/null +++ b/docs/src/content/docs/en/guides/unit-test.mdx @@ -0,0 +1,1572 @@ +--- +title: Writing Unit Tests (UnitTest) +description: Practical guide to designing, structuring, and executing reliable unit tests with CppUtils::UnitTest without external dependencies. +sidebar: + order: 3 +--- + +import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components'; + +Automated unit tests form the foundation of any resilient and maintainable software project. Yet, in the C++ ecosystem, establishing a testing suite historically requires adopting legacy third-party frameworks conceived over fifteen years ago (GoogleTest, Catch2, doctest, Boost.Test, CppUnit). While mature, these tools suffer from major architectural trade-offs: + +- **Proliferation of preprocessor macros**: Traditional frameworks impose macros both to declare test suites and cases (`TEST`, `TEST_F`, `TEST_CASE`) and for each assertion (`ASSERT_EQ`, `CHECK`, etc.). These macros [ignore static typing and namespaces](https://morgancaron.github.io/articles/cpp/preprocessor#typage-et-namespaces), [pollute the global namespace](https://morgancaron.github.io/articles/cpp/preprocessor#pollution-namespace-global), and [impede step-by-step debugging](https://morgancaron.github.io/articles/cpp/preprocessor#debogage-pas-a-pas). Yet C++ cleanly achieves both [test suite registration](#declaring-a-test-suite) and [value diagnostics for assertions](#value-diagnostics) without any preprocessor trickery. +- **Lack of native modularity**: Conceived around legacy header inclusions (`#include`), these tools lack native integration with C++20 modules, increasing compilation times. +- **Lack of dependency scheduling and cascading failure avalanches**: Traditional test frameworks run tests without any awareness of real dependencies between components. If a foundational building block fails (memory allocator, network socket, message queue), all dependent high-level tests collapse simultaneously, flooding the terminal with dozens of secondary failures that mask the genuine root cause. +- **The illusion of mocking as a workaround**: Faced with tight coupling, developers introduce virtual interfaces and mock objects to isolate units artificially. These synthetic abstractions distort real runtime behavior, inflate production code with dynamic dispatch overhead (vtable, smart pointers, heap allocations), and conceal integration regressions, race conditions, and genuine edge cases. + +The [`CppUtils::UnitTest`](/en/reference/unittest/functions/) module reimagines unit testing from the ground up for modern C++26: + + + + Pure declarative syntax with zero preprocessor macros. Eliminates macro code pollution, identifier collisions, and hidden generated code. + + + Deterministic execution via a [Directed Acyclic Graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph). Tests run according to their genuine dependencies, isolating the root cause without mocks. + + + Test conditions directly using standard C++ operators (`==`, `!=`, `<`, etc.). On failure, the exact values compared are automatically displayed in the report without requiring dedicated macros. + + + Every test executes inside a dedicated child process ([`fork()`](#process-isolation-and-timeout-management)). Absolute resilience against fatal crashes (`SIGSEGV`) and infinite loops (enforced by a strict watchdog timer with `SIGKILL`). + + + Verify that expected exceptions are correctly thrown ([`throws`](#throws-matcher-exception-verification)) or guarantee that none occur ([`noThrow`](#nothrow-matcher-exception-absence)), both in the main test thread and across worker threads. + + + Run identical test logic across a collection of types ([`forTypes`](#iterating-over-a-list-of-types-fortypes)) or dynamic values ([`forValues`](#iterating-over-a-list-of-values-forvalues)), configured at the individual test level or across an entire suite. + + + Cleanly bypass tests when the execution environment (OS, network, hardware capabilities) does not satisfy prerequisite criteria ([`skip`](#deliberately-skipping-a-test-skip-and-skipif), [`skipIf`](#deliberately-skipping-a-test-skip-and-skipif)), without failing the suite. + + + Deterministic pseudo-random generation to stress-test algorithms with unexpected payloads, including configurable pseudo-random string and integer sequences. + + + Built-in performance measurements with comparative ratios and automated console warnings when a candidate implementation regresses against baseline standards. + + + Periodic condition polling ([`eventually`](#eventually-matcher-waiting-for-an-asynchronous-condition)) or waiting on an asynchronous operation ([`completes`](#completes-matcher-reactive-synchronization-without-polling)). + + + Validate your `constexpr` contracts and computations directly during compilation ([`suite.test`](#compile-time-tests-suitetestpredicate)). Instant build-time error detection with zero runtime execution overhead. + + + Standard JUnit XML report generation ([`--report`](#junit-xml-report-export-and-cicd-integration)). Native integration in GitLab CI and GitHub Actions to automatically publish metrics and failure annotations in pull requests. + + + +--- + +## Comparison with C++ Testing Frameworks + +`CppUtils::UnitTest` breaks free from the compromises of historical frameworks: zero preprocessor macros, direct expression capture, and automated suite scheduling via a dependency graph. + +### Syntax Comparison + +The interactive tabs below demonstrate an identical testing scenario expressed in each framework: + + + + + +```cpp +// Native C++20 module support (zero macros, zero header files) +import std; +import CppUtils; + +namespace MyApp::Tests +{ + namespace ut = CppUtils::UnitTest; + + // Suite declaration with dependencies + auto _ = ut::testSuite("Math/Calculator").dependsOn("Logger") = [](ut::TestSuite& suite) { + suite.test("Operations") = [] { + // Assertions capturing actual runtime values without macros + ut::expect(add(20, 22)) == 42; + not ut::expect(isNegative(42)); + ut::expect([&] { divide(10, 0); }) == ut::throws; + }; + + // Parameterized test across a collection of values + suite.test("Scale factor").forValues(10, 50, 100) = [](int scale) { + ut::expect(scale) > 0; + }; + }; +} +``` + + +```cpp +// Native C++20 module support (zero macros, zero header files) +import std; +import CppUtils; + +namespace MyApp::Tests +{ + using namespace CppUtils::UnitTest; + + // Suite declaration with dependencies + auto _ = testSuite("Math/Calculator").dependsOn("Logger") = [](TestSuite& suite) { + suite.test("Operations") = [] { + // Assertions capturing actual runtime values without macros + expect(add(20, 22)) == 42; + not expect(isNegative(42)); + expect([&] { divide(10, 0); }) == throws; + }; + + // Parameterized test across a collection of values + suite.test("Scale factor").forValues(10, 50, 100) = [](int scale) { + expect(scale) > 0; + }; + }; +} +``` + + + + +```cpp +import boost.ut; + +int main() { + // Recommended practice by Boost.UT documentation (an alias remains possible to avoid name collisions) + using namespace boost::ut; + + // Test declaration via literal suffix + // No dependency management between suites (linear execution) + "operations"_test = [] { + // Requires literal suffixes (_i) to capture values on failure (limited to basic arithmetic types) + expect(add(20, 22) == 42_i); + expect(not isNegative(42)); + expect(throws([] { divide(10, 0); })); + }; + + // Parameterized test via overloaded pipe operator | + "scale_factor"_test = [](int scale) { + expect(scale > 0_i); + } | std::vector{10, 50, 100}; +} +``` + + +```cpp +// Native C++20 modules not supported (mandatory #include headers) +#include +#include +#include + +// Intrusive macros polluting the global namespace +// No dependency management between suites (linear execution) +TEST_CASE("Math/Calculator Operations", "[math]") { + // Mandatory preprocessor macros to decompose operands + REQUIRE(add(20, 22) == 42); + REQUIRE_FALSE(isNegative(42)); + REQUIRE_THROWS_AS(divide(10, 0), std::invalid_argument); +} + +// Parameterized test: the GENERATE macro re-executes the entire TEST_CASE body +TEST_CASE("Math/Calculator Scale factor", "[math]") { + auto scale = GENERATE(10, 50, 100); + REQUIRE(scale > 0); +} +``` + + +```cpp +// Native C++20 modules not supported (mandatory #include headers) +#include +#include + +// Intrusive macros polluting the global namespace +// No dependency management between suites (linear execution) +TEST(CalculatorTest, Operations) { + EXPECT_EQ(add(20, 22), 42); // Distinct macro required for each operator + EXPECT_FALSE(isNegative(42)); + EXPECT_THROW(divide(10, 0), std::invalid_argument); +} + +// Parameterized test: requires a dedicated fixture class inheriting TestWithParam + 2 verbose macros +class ScaleTest : public testing::TestWithParam {}; + +TEST_P(ScaleTest, PositiveScale) { + EXPECT_GT(GetParam(), 0); +} + +INSTANTIATE_TEST_SUITE_P(Values, ScaleTest, testing::Values(10, 50, 100)); +``` + + +```cpp +// Native C++20 modules not supported (mandatory #include headers) +#include +#include + +// Intrusive macros polluting the global namespace +// No dependency management between suites (linear execution) +TEST_CASE("Math/Calculator") { + SUBCASE("Operations") { + CHECK(add(20, 22) == 42); // Macro-based expression decomposition + CHECK_FALSE(isNegative(42)); + CHECK_THROWS_AS(divide(10, 0), std::invalid_argument); + } + + // Parameterized test: the GENERATE macro re-executes the entire TEST_CASE body (like Catch2) + SUBCASE("Scale factor") { + auto scale = GENERATE(10, 50, 100); + CHECK(scale > 0); + } +} +``` + + +```cpp +// Native C++20 modules not supported (mandatory #include headers) +#define BOOST_TEST_MODULE CalculatorTest +#include +#include +#include + +// Intrusive macros polluting the global namespace +// No dependency management between suites (linear execution) +// Hidden scope (namespace) injected without visible braces, enclosing any intermediate code +BOOST_AUTO_TEST_SUITE(Calculator) + +BOOST_AUTO_TEST_CASE(Operations) { + BOOST_TEST(add(20, 22) == 42); + BOOST_TEST(!isNegative(42)); + BOOST_CHECK_THROW(divide(10, 0), std::invalid_argument); +} + +// Parameterized test: requires heavy and complex dataset macro +BOOST_DATA_TEST_CASE(ScaleFactor, boost::unit_test::data::make({10, 50, 100}), scale) { + BOOST_TEST(scale > 0); +} + +BOOST_AUTO_TEST_SUITE_END() // Mandatory closure of the hidden scope opened by the suite macro +``` + + +```cpp +// Native C++20 modules not supported (mandatory #include headers) +#include +#include + +// Verbose declaration via class inheriting QObject +// No dependency management between suites (linear execution) +class CalculatorTest : public QObject { + Q_OBJECT +private slots: + void testOperations() { + QCOMPARE(add(20, 22), 42); // Forces tested types to adapt to the test framework by requiring overloads of the non-standard proprietary QTest::toString API for failure diagnostics + QVERIFY(!isNegative(42)); + QVERIFY_THROWS_EXCEPTION(std::invalid_argument, divide(10, 0)); + } + + // Parameterized test: requires 2 distinct methods and a dynamic table + void testScale_data() { + QTest::addColumn("scale"); + QTest::newRow("10") << 10; + QTest::newRow("50") << 50; + QTest::newRow("100") << 100; + } + + void testScale() { + QFETCH(int, scale); + QVERIFY(scale > 0); + } +}; + +QTEST_MAIN(CalculatorTest) + +// Mandatory inclusion of MOC-generated code: reliance on a proprietary preprocessor injecting invisible code and applying non-standard "magic" ('Q_OBJECT', slots) to introspect tests +#include "CalculatorTest.moc" +``` + + +```cpp +// Native C++20 modules not supported (mandatory #include headers) +#include "CppUnitTest.h" +#include + +// Microsoft's official practice to access the Assert:: class, deliberately polluting the global namespace with their entire proprietary framework +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +// Intrusive macros polluting the global namespace +// No dependency management between suites (linear execution) +TEST_CLASS(CalculatorTest) { +public: + TEST_METHOD(TestOperations) { + Assert::AreEqual(42, add(20, 22)); + Assert::IsFalse(isNegative(42)); + Assert::ExpectException([] { divide(10, 0); }); + } + + // Parameterized test: no native mechanism, imperative loop without isolation + TEST_METHOD(TestScale) { + for (int scale : {10, 50, 100}) + Assert::IsTrue(scale > 0); + } +}; +``` + + +```cpp +// Native C++20 modules not supported (mandatory #include headers) +#include +#include + +// Intrusive macros and verbose fixture class +// No dependency management between suites (linear execution) +class CalculatorTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(CalculatorTest); + CPPUNIT_TEST(testOperations); + CPPUNIT_TEST(testScale); + CPPUNIT_TEST_SUITE_END(); + +public: + void testOperations() { + CPPUNIT_ASSERT_EQUAL(42, add(20, 22)); // Assertion macros lacking modern decomposition + CPPUNIT_ASSERT(!isNegative(42)); + CPPUNIT_ASSERT_THROW(divide(10, 0), std::invalid_argument); + } + + // Parameterized test: no native support, imperative loop without isolation + void testScale() { + for (int scale : {10, 50, 100}) + CPPUNIT_ASSERT(scale > 0); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(CalculatorTest); +``` + + + + + +### Feature Comparison Matrix + +| Feature | `CppUtils::UnitTest` | Boost.UT | Catch2 (v3) | Google Test | doctest | Boost.Test | Qt Test | MS Test (C++) | CppUnit | +| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| **Zero preprocessor macros** | ✅ | ✅ | ❌ ([`REQUIRE`](https://github.com/catchorg/Catch2/blob/devel/docs/assertions.md)) | ❌ ([`EXPECT_EQ`](https://google.github.io/googletest/reference/assertions.html)) | ❌ ([`CHECK`](https://github.com/doctest/doctest/blob/master/doc/markdown/assertions.md)) | ❌ ([`BOOST_CHECK`](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/testing_tools/summary.html)) | ❌ ([`QCOMPARE`](https://doc.qt.io/qt-6/qtest.html#QCOMPARE)) | ❌ ([`TEST_METHOD`](https://learn.microsoft.com/visualstudio/test/microsoft-visualstudio-testtools-cppunittestframework-api-reference#test_macros)) | ❌ ([`CPPUNIT_`](https://freedesktop.org/wiki/Software/cppunit/)) | +| **Native C++20 modules** | ✅ (`import`) | ✅ ([`ut.cppm`](https://github.com/boost-ext/ut/blob/master/include/boost/ut.cppm)) | ❌ (`#include` headers) | ❌ (`#include` headers) | ❌ (`#include` headers) | ❌ (`#include` headers) | ❌ (`#include` headers) | ❌ (`#include` headers) | ❌ (`#include` headers) | +| **Expression decomposition (`lhs == rhs`)** | ✅ (`expect(a) == b` syntax) | ✅ (`1_i == 2` or `that %` / `eq`) | ⚠️ Partial (macro decomposition) | ❌ (dedicated macros [`ASSERT_EQ`](https://google.github.io/googletest/reference/assertions.html)) | ⚠️ Partial (macro decomposition) | ⚠️ Partial (via [`BOOST_TEST`](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/testing_tools/extended_comparison/test.html)) | ❌ ([`QCOMPARE`](https://doc.qt.io/qt-6/qtest.html#QCOMPARE)) | ❌ ([`Assert::AreEqual`](https://learn.microsoft.com/visualstudio/test/microsoft-visualstudio-testtools-cppunittestframework-api-reference#assert_classes)) | ❌ ([`CPPUNIT_ASSERT_EQUAL`](https://cppunit.sourceforge.net/doc/lastest/group___assertions.html#ga71162f02be052994ea26d15cb9115ab5)) | +| **Mocking** | ❌ ([learn why](#why-no-mocking-library-is-provided)) | ❌ | ❌ (via third-party) | ✅ ([GoogleMock](https://google.github.io/googletest/gmock_for_dummies.html)) | ❌ (via third-party) | ❌ (via third-party) | ⚠️ Partial ([`QSignalSpy`](https://doc.qt.io/qt-6/qsignalspy.html)) | ❌ | ❌ | +| **Dependency scheduling** | ✅ (topological ordering & automatic failure skipping) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Integrated benchmarks & comparison** | ✅ (`benchmark`, `warnIfSlower`) | ❌ | ⚠️ Partial ([`BENCHMARK`](https://github.com/catchorg/Catch2/blob/devel/docs/benchmarks.md)) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Parameterized tests (`forValues`)** | ✅ | ✅ | ✅ ([`GENERATE`](https://github.com/catchorg/Catch2/blob/devel/docs/generators.md)) | ✅ ([`INSTANTIATE_TEST_SUITE_P`](https://google.github.io/googletest/advanced.html#value-parameterized-tests)) | ✅ ([`GENERATE`](https://github.com/doctest/doctest/blob/master/doc/markdown/parameterized-tests.md)) | ✅ ([datasets](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/tests_organization/test_cases/test_case_generation.html)) | ⚠️ Partial ([`QTest::newRow`](https://doc.qt.io/qt-6/qtest-overview.html#data-driven-testing)) | ❌ | ❌ | +| **Typed tests (`forTypes`)** | ✅ (variadic direct) | ✅ | ✅ ([`TEMPLATE_TEST_CASE`](https://github.com/catchorg/Catch2/blob/devel/docs/test-cases-and-sections.md#type-parametrised-test-cases)) | ✅ ([`TYPED_TEST`](https://google.github.io/googletest/advanced.html#typed-tests)) | ⚠️ Partial ([`TYPE_TO_STRING`](https://github.com/doctest/doctest/blob/master/doc/markdown/parameterized-tests.md)) | ✅ ([template test cases](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/tests_organization/test_cases/test_case_generation/datasets_auto_creation/types.html)) | ❌ | ❌ | ❌ | +| **Deterministic fuzzing engine** | ✅ (with repeatable seed) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Process isolation (`fork`)** | ✅ (`SIGSEGV` resilience) | ❌ | ❌ | ⚠️ Partial ([`death_test`](https://google.github.io/googletest/advanced.html#death-tests)) | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Asynchronous verification (`eventually`, `completes`)** | ✅ (periodic polling and reactive wake) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **JUnit XML reports (CI/CD)** | ✅ (native via `--report=junit:...`) | ❌ | ✅ ([`-r junit`](https://github.com/catchorg/Catch2/blob/devel/docs/reporters.md#junit-reporter)) | ✅ ([`--gtest_output=xml`](https://google.github.io/googletest/advanced.html#generating-an-xml-report)) | ✅ ([`-r=junit`](https://github.com/doctest/doctest/blob/master/doc/markdown/reporters.md#reporters-and-listeners)) | ✅ ([`--log_format=JUNIT`](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/utf_reference/rt_param_reference/log_format.html)) | ✅ ([`-junitxml`](https://doc.qt.io/qt-6/qtest-overview.html#qtest-command-line-arguments)) | ⚠️ Partial ([proprietary `.trx`](https://learn.microsoft.com/visualstudio/test/vstest-console-options), JUnit via third-party plugin) | ✅ ([`XmlOutputter`](https://cppunit.sourceforge.net/doc/lastest/class_xml_outputter.html)) | +| **Zero external dependencies** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ ([Boost](https://www.boost.org/)) | ❌ ([Qt Core & MOC](https://doc.qt.io/qt-6/qtest.html)) | ❌ ([Visual Studio](https://learn.microsoft.com/visualstudio/test/writing-unit-tests-for-c-cpp)) | ✅ | + +### Why This Architecture Makes the Difference + +- **Code clarity and modern tooling**: Eliminating macros entirely enables perfect IDE code completion, reliable automated refactoring, and step-by-step debugging without hidden generated code. +- **Positioning relative to modern alternatives**: Among all existing frameworks, **Boost.UT** represents the most advanced and respectable effort to break free from macros. `CppUtils::UnitTest` preserves that modern spirit while removing the remaining friction: zero requirement for dedicated literal suffixes (`_i`) to capture any standard type, native dependency graph scheduling, and process-level crash resilience. +- **Immediate root cause isolation**: Thanks to the dependency graph (`CppUtils::Container::DependencyGraph`), if a foundational module fails, hundreds of dependent high-level tests are automatically skipped instead of overwhelming the terminal with noise. +- **Unified software quality**: Instead of juggling Google Test for unit tests, Google Benchmark for performance profiling, libFuzzer for fuzzing, and custom scripts for crash isolation, `CppUtils::UnitTest` consolidates all these pillars into a single, cohesive, and expressive API. + +--- + +## Declaring a Test Suite + +A test suite is declared simply: + +```cpp +import std; +import CppUtils; + +namespace MyApp::Tests +{ + namespace ut = CppUtils::UnitTest; + + auto _ = ut::testSuite("Math/Calculator") = [](ut::TestSuite& suite) { + using namespace std::literals; + + suite.test("Addition") = [] { + const auto result = 20 + 22; + ut::expect(result) == 42; + }; + + suite.test("Heavy computation").timeout(30s) = [] { + ut::expect(performHeavyComputation()) > 0; + }; + }; +} +``` + +### Dependencies Between Test Suites + +When a test suite requires other test suites to pass before running, declare them with `.dependsOn(...)`: + +```cpp +auto _ = ut::testSuite("Network/Client").dependsOn("Logger", "Network/Socket") = [](ut::TestSuite& suite) { + suite.test("Connection") = [] { + // This test only executes if "Logger" and "Network/Socket" succeeded + }; +}; +``` + +```mermaid +flowchart TD + subgraph Prerequisites["Prerequisite Suites"] + Logger["Logger"] + Socket["Network/Socket"] + end + + subgraph Dependent["Dependent Suite"] + Client["Network/Client"] + end + + Logger -->|"Success required"| Client + Socket -->|"Success required"| Client +``` + +By declaring these dependencies, the runner structures test execution as a [Directed Acyclic Graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph). This graph structure provides three major benefits: +- **Immediate root-cause identification**: If `Network/Socket` fails, `Network/Client` is not even executed and is marked as skipped due to unmet prerequisites. The genuine failure is never buried beneath an avalanche of cascading secondary errors caused by a broken upstream dependency. +- **Clean terminal output with `failFast`**: When [`failFast`](#configuration-options-cpputilsunittesttestsettings) is enabled, the runner stops immediately on the first root failure without running dependent suites. +- **Eliminating the need for mocking (*mocks / stubs*)**: Because tests execute in the strict topological order of their genuine dependencies and failures are isolated at the source, introducing artificial test doubles to mask upstream fragility becomes unnecessary. You test genuine components end-to-end with full confidence. + +--- + +## Why No Mocking Library Is Provided + +The deliberate omission of a mocking library in CppUtils is an intentional design choice. In the C++ ecosystem, mocking introduces two fundamental problems: + +### 1. The Illusion of Testing and False Guarantees + +A mock replaces genuine components with canned responses. This gives the illusion of testing a component, while the real integration has **never actually run**: +- **Networking example**: If you mock a socket to return a pre-scripted JSON payload, you validate parsing under ideal conditions, but completely miss how your component interacts with real stream dynamics: fragmented packets, state desynchronization, or race conditions when data arrives during an internal state transition. These concurrent operations can collide in production, causing issues that a static mock will never reveal. +- **Filesystem example**: If you mock filesystem calls, you bypass OS file locks, actual user permissions, and physical disk synchronization. + +In production, the system operates in a scenario that has **never truly been tested**. CppUtils encourages testing against real conditions: launching an actual local server on an ephemeral port or creating genuine temporary files. + +### 2. Distortion of Production Code (*Zero-Cost Abstraction*) + +Making a class "mockable" often forces developers to compromise production design: imposing object-oriented (OOP) class hierarchies for dependency injection, multiplying virtual interfaces (`virtual` / vtables), and relying on dynamic allocations (`std::unique_ptr`). These indirections inhibit compiler inlining and add runtime overhead solely to satisfy the testing harness. + +### The Alternative: Dependency-Graph Scheduling + +The perceived need for mocks often stems from fear that upstream failures will trigger cascading test crashes, making it difficult to pinpoint the root cause of the issue. With [`CppUtils::Container::DependencyGraph`](/en/reference/container/dependencygraph/), CppUtils resolves this: foundational modules [run first](#dependencies-between-test-suites). If a dependency fails, dependent suites are automatically skipped, pinpointing the root cause without resorting to artificial simulations. + +--- + +## Writing Assertions + +Unlike traditional frameworks that force developers to memorize dozens of specialized macros depending on the type of check ([`ASSERT_EQ`](https://google.github.io/googletest/reference/assertions.html#EXPECT_EQ), [`ASSERT_NE`](https://google.github.io/googletest/reference/assertions.html#EXPECT_NE), [`ASSERT_THROW`](https://google.github.io/googletest/reference/assertions.html#EXPECT_THROW), [`CHECK_FALSE`](https://github.com/catchorg/Catch2/blob/devel/docs/assertions.md), etc. (see [Comparison](#comparison-with-c-testing-frameworks))), `CppUtils::UnitTest` prioritizes simplicity with **a single assertion function: `expect(...)`**. + +### Boolean Assertion: `expect` + +The `expect` function validates that a condition or expression is true: + +```cpp +ut::expect(database.isConnected()); +ut::expect(not std::ranges::empty(buffer)); +``` + +Writing `== true` is unnecessary: `expect(condition);` directly evaluates the condition. + +Upon failure, the report pinpoints the exact source file, line number, column, and the evaluated runtime value: + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Database/Connection/Ping:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Database/Connection/Ping + In tests/Database/Connection.mpp +At line 42, column 5 +In expect(condition) +with value: +[red]false[/red] + +[red]Failed ✕[/red] +``` + +#### Objects Convertible to Boolean + +Any type convertible to `bool` (`std::optional`, raw pointers, `std::unique_ptr`, `std::shared_ptr`, types with `explicit operator bool()`) is automatically converted and evaluated as a boolean by `expect`: + +```cpp +auto user = std::optional{fetchUser(42)}; +ut::expect(user); + +auto connection = std::unique_ptr{connect()}; +ut::expect(connection); +``` + +Upon failure on a null pointer or empty `std::optional`, the report directly outputs `with value: nullptr` or `with value: nullopt` in red: + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Database/Connection/Open:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Database/Connection/Open + In tests/Database/Connection.mpp +At line 42, column 2 +In expect(condition) +with value: +[red]nullopt[/red] + +[red]Failed ✕[/red] +``` + +#### Negation: `not expect(...)` over `expect(not ...)` + +To assert that a condition must be false, prefer placing the negation on the outside: + +```cpp +// Recommended: +not expect(session.isExpired()); + +// Equivalent: +expect(session.isExpired()) == false; +``` + +While writing `expect(not condition);` works in C++, external negation provides significantly richer diagnostics upon failure: + +- With `expect(not error);`: the logical inversion is resolved *before* entering `expect`. The expression is immediately collapsed into a raw `bool`. Upon failure, the report merely shows `with value: false` without revealing the object's contents: + +```cpp +auto error = std::optional{"Connection refused"}; +expect(not error); +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Connection:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Connection + In tests/Network/Connection.mpp +At line 42, column 5 +In expect(condition) +with value: +[red]false[/red] + +[red]Failed ✕[/red] +``` + +- With `not expect(error);`: the framework captures the original object intact. For any dereferenceable type (`operator*`) whose underlying value is printable, the diagnostic unpacks and displays the actual unexpected element that caused the failure in red: + +```cpp +auto error = std::optional{"Connection refused"}; +not expect(error); +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Connection:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Connection + In tests/Network/Connection.mpp +At line 42, column 5 +In not expect(condition) +with value: +[red]Connection refused[/red] + +[red]Failed ✕[/red] +``` + +### Value Diagnostics + +In traditional testing libraries, inspecting operand values forces developers to abandon idiomatic C++ operators in favor of an overwhelming zoo of specialized macros ([`ASSERT_EQ`](https://google.github.io/googletest/reference/assertions.html#EXPECT_EQ), [`ASSERT_NE`](https://google.github.io/googletest/reference/assertions.html#EXPECT_NE), [`ASSERT_LT`](https://google.github.io/googletest/reference/assertions.html#EXPECT_LT), [`ASSERT_LE`](https://google.github.io/googletest/reference/assertions.html#EXPECT_LE), [`ASSERT_GT`](https://google.github.io/googletest/reference/assertions.html#EXPECT_GT), [`ASSERT_GE`](https://google.github.io/googletest/reference/assertions.html#EXPECT_GE), [`EXPECT_FLOAT_EQ`](https://google.github.io/googletest/reference/assertions.html#EXPECT_FLOAT_EQ), etc.). + +With `CppUtils::UnitTest`, standard C++ comparison operators replace all these macros without verbose wrappers or [proprietary literals](https://boost-ext.github.io/ut/): + +```cpp +ut::expect(left) == right; +ut::expect(left) != right; +ut::expect(left) < right; +ut::expect(left) <= right; +ut::expect(left) > right; +ut::expect(left) >= right; +``` + +```cpp +namespace ut = CppUtils::UnitTest; + +auto _ = ut::testSuite("Auth/Session") = [](ut::TestSuite& suite) { + using namespace std::chrono_literals; + + suite.test("Validate session") = [] { + ut::expect(session.getRemainingTime()) > 0s; + ut::expect(session.getUserRole()) == "admin"; + ut::expect(session.getFailedAttempts()) <= 3u; + ut::expect(std::ranges::size(session.getActiveTokens())) == 2uz; + }; +}; +``` + +#### Why This Syntax Was Chosen + +1. **Pure C++23 with zero preprocessor macros**: In frameworks like Catch2 or GoogleTest, expression decomposition strictly requires macros (`REQUIRE(a == b)`, `EXPECT_EQ(a, b)`). In pure C++, writing `expect(a == b);` would pass an already-evaluated `bool` to `expect`, making separate inspection of `a` and `b` impossible. By writing `expect(a) == b;`, `expect(a)` returns a temporary proxy object (`ExpectationProxy`) that intercepts the subsequent comparison operator, capturing both left-hand (`lhs`) and right-hand (`rhs`) operands along with their operator symbol without a single macro. +2. **Zero identifier collisions and variable shadowing**: Approaches relying on free functions such as `value(...)` or `that(...)` cause major conflicts whenever a test declares a local variable sharing that common name. Writing `auto value = calculate(); expect(value(value) == 42);` produces a fatal compilation error (`error: type does not provide a call operator`). With `expect(value) == 42;`, the variable `value` is passed directly with no risk of shadowing or ambiguity. +3. **Zero pollution from custom test literals**: To intercept comparisons without macros, [Boost.UT](https://boost-ext.github.io/ut/) must force at least one operand into an intermediate framework type: either via proprietary literal suffixes (`42_i == sum()`), limited to basic arithmetic types, or via a matcher wrapper (`expect(that % 0 == sum());`), or via prefixed named functions (`expect(eq(a, b));`). `CppUtils::UnitTest` solves this in a unified and generic way without compromises: zero imposed literals, zero verbose wrappers, and zero prefixed functions. Any modern C++ type is naturally captured with standard language operators. +4. **Direct Readability**: The assertion reads from left to right ("expect left equals right") without redundant prefixes. +5. **Assertions Without Relational Operators**: Boolean conditions evaluate directly (e.g. `ut::expect(user.isConnected());`) without requiring comparison operators or a redundant `== true` suffix. + +Upon failure (for instance if `user.getRole()` returns `"guest"` instead of `"admin"`), the diagnostic decomposes the expression and prints the exact runtime values of both operands: + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]UnitTest/Auth:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Auth/Session/Admin verification + In tests/Auth/Session.mpp +At line 42, column 2 +In expect(lhs) == rhs +with lhs: +guest +and rhs: +admin + +Diff: + lhs (-): "[red]guest[/red]" + rhs (+): "[green]admin[/green]" + +[red]Failed ✕[/red] +``` + + + +For isolated differences within long strings, this diff calculation pinpoints the mismatched characters in place: + +```cpp +const auto expectedUrl = "https://api.example.com/v1/users/42/profile"; +const auto actualUrl = "https://api.example.com/v2/users/42/profile"; +ut::expect(expectedUrl) == actualUrl; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Api/EndpointCheck:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Api/EndpointCheck + In tests/Network/Api.mpp +At line 18, column 2 +In expect(lhs) == rhs +with lhs: +https://api.example.com/v1/users/42/profile +and rhs: +https://api.example.com/v2/users/42/profile + +Diff: + lhs (-): "https://api.example.com/v[red]1[/red]/users/42/profile" + rhs (+): "https://api.example.com/v[green]2[/green]/users/42/profile" + +[red]Failed ✕[/red] +``` + +*(As shown above, only the `1` appears in red and the `2` in green, while the rest of the URL remains in standard neutral color).* + +Likewise, when multiple segments differ within a sentence, each mismatch is highlighted individually without merging the unchanged intermediate text: + +```cpp +const auto expectedSentence = "The quick brown fox jumps over the lazy dog"; +const auto actualSentence = "The fast brown fox leaps over a lazy dog"; +ut::expect(expectedSentence) == actualSentence; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]UnitTest/VisualColorInspection/Diff visuel complexe (segments multiples):[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +UnitTest/VisualColorInspection/Diff visuel complexe (segments multiples) + In tests/UnitTest/UnitTest.mpp +At line 711, column 5 +In expect(lhs) == rhs +with lhs: +The quick brown fox jumps over the lazy dog +and rhs: +The fast brown fox leaps over a lazy dog + +Diff: + lhs (-): "The [red]quick[/red] brown fox [red]jum[/red]ps over [red]the[/red] lazy dog" + rhs (+): "The [green]fast[/green] brown fox [green]lea[/green]ps over [green]a[/green] lazy dog" + +[red]Failed ✕[/red] +``` + +### Native Complex Type Formatting + +The inspection engine relies on generic C++20 concepts, without requiring serialization macros or custom display overloads (unlike, for instance, [`QTest::toString`](https://doc.qt.io/qt-6/qtest.html#toString) in Qt Test). Any type that satisfies these concepts (iterable ranges, dereferenceable types, tuple-like structures, etc.), whether from the standard library, a third-party library, or your own code, is automatically analyzed and formatted. + +#### Containers and Ranges + +Containers (`std::vector`, `std::span`, lists, views) are compared element by element using `std::ranges::equal` and formatted as readable lists: + +```cpp +const auto headers = std::vector{"content-type", "authorization"}; +ut::expect(headers) == std::vector{"content-type", "accept"}; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Headers/Validate:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Headers/Validate + In tests/Network/Headers.mpp +At line 32, column 2 +In expect(lhs) == rhs +with lhs: +["content-type", "authorization"] +and rhs: +["content-type", "accept"] + +Diff: + lhs (-): ["content-type", "[red]authorization[/red]"] + rhs (+): ["content-type", "[green]accept[/green]"] + +[red]Failed ✕[/red] +``` + +#### Tuples and Pairs + +Composite `std::tuple` and `std::pair` structures are decomposed and formatted recursively field by field: + +```cpp +const auto response = std::make_pair(404, "Not Found"); +ut::expect(response) == std::make_pair(200, "OK"); +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Response/Validate:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Response/Validate + In tests/Network/Response.mpp +At line 15, column 2 +In expect(lhs) == rhs +with lhs: +(404, "Not Found") +and rhs: +(200, "OK") + +Diff: + lhs (-): ([red]404[/red], "[red]Not Found[/red]") + rhs (+): ([green]200[/green], "[green]OK[/green]") + +[red]Failed ✕[/red] +``` + +#### Dereferenceable and Optional Types + +`std::optional`, `std::unique_ptr`, and `std::shared_ptr` are inspected automatically, explicitly displaying `nullopt` or `nullptr` when empty, or the underlying dereferenced value: + +```cpp +const auto cachedUser = std::optional{std::nullopt}; +ut::expect(cachedUser) == std::optional{"alice"}; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Auth/Cache/Lookup:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Auth/Cache/Lookup + In tests/Auth/Cache.mpp +At line 24, column 2 +In expect(lhs) == rhs +with lhs: +[red]nullopt[/red] +and rhs: +"alice" + +[red]Failed ✕[/red] +``` + +#### Raw Memory Bytes (`std::byte`) and Binary Buffers + +Memory bytes are formatted in explicit hexadecimal notation (`0x2A`), including when stored in collections (`std::vector`, `std::span`), ideal for validating binary protocols and network packet headers: + +```cpp +const auto header = std::vector{std::byte{0x7F}, std::byte{0x45}, std::byte{0x4C}, std::byte{0x46}}; // ELF magic +ut::expect(header) == std::vector{std::byte{0x7F}, std::byte{0x45}, std::byte{0x00}, std::byte{0x00}}; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Binary/Header/Magic:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Binary/Header/Magic + In tests/Binary/Header.mpp +At line 12, column 2 +In expect(lhs) == rhs +with lhs: +[0x7F, 0x45, 0x4C, 0x46] +and rhs: +[0x7F, 0x45, 0x00, 0x00] + +Diff: + lhs (-): [0x7F, 0x45, [red]0x4C[/red], [red]0x46[/red]] + rhs (+): [0x7F, 0x45, [green]0x00[/green], [green]0x00[/green]] + +[red]Failed ✕[/red] +``` + +#### Filesystem Paths (`std::filesystem::path`) + +Filesystem paths are normalized and displayed in their canonical string format: + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]FileSystem/Config/PathCheck:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +FileSystem/Config/PathCheck + In tests/FileSystem/Config.mpp +At line 28, column 2 +In expect(lhs) == rhs +with lhs: +"/var/log/syslog" +and rhs: +"/var/log/app.log" + +Diff: + lhs (-): "/var/log/[red]sys[/red]log" + rhs (+): "/var/log/[green]app.[/green]log" + +[red]Failed ✕[/red] +``` + +#### Floating-Point Numbers with Automatic Epsilon Tolerance + +Floating-point comparisons (`float`, `double`) automatically use relative epsilon tolerance ([`Math::isEqual`](/en/reference/math/functions/#isEqual)), eliminating IEEE 754 precision failures without requiring dedicated macros ([`EXPECT_NEAR`](https://google.github.io/googletest/reference/assertions.html#EXPECT_NEAR)): + +```cpp +ut::expect(0.1 + 0.2) == 0.3; // Passes automatically despite IEEE 754 precision limits +``` + +#### Heterogeneous Integers (Signed / Unsigned) + +Mixed comparisons rely on standard library safe comparison utilities (`std::cmp_equal`, `std::cmp_greater`, etc.), avoiding risky implicit conversions and `-Wsign-compare` compiler warnings: + +```cpp +ut::expect(-1) > 1u; // False in standard C++ due to unsigned promotion; correctly evaluated here +``` + +#### Character Strings and Literals + +Comparing against text literals evaluates character content via `std::string_view`, eliminating pointer address comparison bugs. + +### Matchers: `throws`, `noThrow`, `crashes`, `eventually`, `completes` + +Rather than calling verbose and scattered assertion functions or macros ([`ASSERT_THROW`](https://google.github.io/googletest/reference/assertions.html#EXPECT_THROW), [`ASSERT_NO_THROW`](https://google.github.io/googletest/reference/assertions.html#EXPECT_NO_THROW), [`ASSERT_DEATH`](https://google.github.io/googletest/reference/assertions.html#death-assertions), etc.), `CppUtils::UnitTest` unifies checks using the standard `==` operator on `expect(action)`: + +#### `throws` Matcher: Exception Verification + +Exception type validation: + +```cpp +ut::expect([&] { + computeSquareRoot(-1.0); +}) == ut::throws; +``` + +Validating that an error message contains a substring: + +```cpp +ut::expect([&] { + divide(10, 0); +}) == ut::throws("Division by zero"); +// Passes if exception.what() contains "Division by zero" (e.g. "Math error: Division by zero detected") +``` + +Boolean predicate validation: + +```cpp +ut::expect([&] { + database.connect("invalid_host"); +}) == ut::throws([](const DatabaseException& error) { + return error.code == 404 and error.message.contains("unreachable"); +}); +``` + +Direct value equality matching: + +```cpp +ut::expect([&] { + parser.parse("!"); +}) == ut::throws(ParseError{.line = 1, .code = ErrorCode::UnexpectedToken}); +``` + +Inspector validation with internal assertions: + +```cpp +ut::expect([&] { + database.connect("invalid_host"); +}) == ut::throws([&](const DatabaseException& error) { + ut::expect(error.code) == 404; + ut::expect(error.message.contains("unreachable")); +}); +``` + +#### `noThrow` Matcher: Exception Absence + +Verifies that no exception is thrown during execution: + +```cpp +ut::expect([&] { + socket.receive(); +}) == ut::noThrow; +``` + +#### `crashes` Matcher: Crash and Critical Signal Interception + +Fatal termination validation (any fatal termination signal): + +```cpp +ut::expect([&] { + int* nullPointer = nullptr; + *nullPointer = 42; +}) == ut::crashes; +``` + +Specific fatal signal validation: + +```cpp +ut::expect([&] { + std::abort(); +}) == ut::crashes(CppUtils::System::Signal::Abort); +``` + +#### `eventually` Matcher: Waiting for an Asynchronous Condition + +Periodically evaluates an asynchronous condition until it becomes true or a deadline is reached. + +By default, `eventually` waits up to **1 second** with a **20 ms** polling interval: + +```cpp +// Using default settings (timeout: 1s, step: 20ms) +ut::expect([&] { + return counter.load() == 5uz; +}) == ut::eventually; +``` + +It can also be customized with a custom deadline alone, or alongside a custom polling interval: + +```cpp +// Custom deadline of 500ms (retains default 20ms interval) +ut::expect([&] { + return isReady.load(); +}) == ut::eventually(500ms); + +// Custom deadline (500ms) and custom polling interval (10ms) +ut::expect([&] { + return counter.load() == 5uz; +}) == ut::eventually(500ms, 10ms); +``` + +#### `completes` Matcher: Reactive Synchronization Without Polling + +Waits for an asynchronous operation or synchronization primitive to complete before a maximum deadline (1 second by default). + +Waiting on a `std::future`: + +```cpp +auto promise = std::promise{}; +auto future = promise.get_future(); +worker.execute([&] { promise.set_value(42); }); + +// Validate completion only +ut::expect(future) == ut::completes; + +// Combined check for completion and returned value +ut::expect(future) == ut::completes.with(42); + +// Custom timeout duration +ut::expect(future) == ut::completes(500ms).with(42); +``` + +With a lambda returning an asynchronous result: + +```cpp +ut::expect([&] { return worker.computeAsync(); }) == ut::completes.with(42); +``` + +With a `std::shared_future`: + +```cpp +auto sharedFuture = promise.get_future().share(); +ut::expect(sharedFuture) == ut::completes.with(42); +``` + +With a C++20 semaphore (`std::binary_semaphore`, `std::counting_semaphore`): + +```cpp +auto signal = std::binary_semaphore{0}; +scheduler.schedule([&] { signal.release(); }, 50ms); + +ut::expect(signal) == ut::completes; +``` + +--- + + + +### Deliberately Skipping a Test: `skip` and `skipIf` + +When a test is temporarily disabled or requires a specific environment (specific operating system, required hardware resource, remote network connectivity, etc.), you can cleanly skip it without failing the test suite: + +```cpp +// Skip a test unconditionally (e.g. work in progress or pending bugfix) +skip("Feature under active refactoring (issue #142)"); + +// Skip a test based on a condition +skipIf(not isNetworkAvailable(), "Remote network access required"); +``` + +--- + +## Compile-Time Tests (`suite.test`) + +To validate `constexpr` algorithms, template metaprogramming, concepts, or static constants, the test framework provides a direct compile-time overload for `suite.test`: + +```cpp +suite.test<[] { + constexpr auto value = factorial(5); + return value == 120; +}>("Factorial computation"); +``` + +This mechanism combines the rigor of `static_assert` with full test runner integration: +- **Immediate static feedback without rerunning tests**: If the predicate fails, compilation halts immediately with a `static_assert` error before any binary execution. You know the test passes as soon as compilation succeeds, without needing to launch the test suite. +- **Runner reporting and metrics**: The compile-time test is accounted for in the suite summary and prints its status (`Passed ✓`). + + + +--- + +## Robustness Testing and Fuzzing (`fuzzTest` and `fuzz`) + +The framework incorporates a deterministic pseudo-random fuzzing engine. This determinism guarantees that each test execution replays strictly identical input data sequences, delivering total reproducibility across continuous integration runs: + +```cpp +// Executes 5,000 iterations with pseudo-randomly regenerated values on each run +suite.fuzzTest("Parser fuzzing", 5'000uz) = [](auto& fuzzer) { + // Generates a pseudo-random string between 1 and 64 characters in length + const auto input = fuzzer.randomString(1uz, 64uz); + + // Validate that the parser experiences no undefined behavior or crashes + parseData(input); +}; +``` + +You can also inject pseudo-random inputs inside standard test cases using `fuzz`: + +```cpp +suite.test("Integer arithmetic fuzzing") = [] { + fuzz("Random arithmetic checks", 1'000uz, [](auto& fuzzer) { + const auto a = fuzzer.random(-1000, 1000); + // Lower bound at 1 to exclude division by zero + const auto b = fuzzer.random(1, 1000); + expect((a / b) * b + (a % b)) == a; + }); +}; +``` + +### Available Pseudo-Random Generators + +The `fuzzer` object (an instance of [`CppUtils::Math::DeterministicRandomEngine`](/en/reference/math/deterministicrandomengine/)) provides a complete set of deterministic, fully `constexpr`-compatible generation methods: + +| Method & Signature | Description | +| :--- | :--- | +| [`random()`](/en/reference/math/deterministicrandomengine/#random_2) | Generates a pseudo-random value of the requested type (`int`, `size_t`, `double`, `bool`, etc.) covering default minimum and maximum type bounds. | +| [`random(min, max)`](/en/reference/math/deterministicrandomengine/#random) | Generates a bounded pseudo-random value in the range `[min, max]` with automatic type deduction (integral or floating-point). | +| [`randomSize(max)`](/en/reference/math/deterministicrandomengine/#randomSize_2) | Convenient shorthand generating a `std::size_t` in `[0, max]` (or `[min, max]` with 2 arguments). Parameterless overload defaults to `PTRDIFF_MAX` [matching the maximum legal contiguous object size in C++](https://morgancaron.github.io/articles/cpp/size#limitation-de-la-plage-de-valeurs). | +| [`randomFloat(min, max)`](/en/reference/math/deterministicrandomengine/#randomFloat) | Generates a floating-point number in the range `[min, max]` (defaults: `0.0` to `1.0`). | +| [`randomString(minLen, maxLen, alphabet)`](/en/reference/math/deterministicrandomengine/#randomString) | Generates a variable-length string (defaults: 0 to 32 characters) from the provided character set. | +| [`randomBytes(count)`](/en/reference/math/deterministicrandomengine/#randomBytes) | Allocates and fills a vector of `count` bytes (`std::vector`). | +| [`pickOne(collection)`](/en/reference/math/deterministicrandomengine/#pickOne) | Selects a random element from a collection. | + +For full parameter details and overloads, consult the [`DeterministicRandomEngine` API reference](/en/reference/math/deterministicrandomengine/). + +### Customizing Random Strings + +The `alphabet` parameter of `randomString` enables targeting specific string formats and token types: + +```cpp +// Fixed length of 32 characters (min=32, max=32) drawn from the hexadecimal alphabet (hash, UUID token) +auto hexToken = fuzzer.randomString(32uz, 32uz, "0123456789abcdef"); + +// Variable length from 1 to 10 characters restricted to digits 0-9 +auto numericId = fuzzer.randomString(1uz, 10uz, "0123456789"); + +// Variable length from 5 to 20 special characters to stress test a parser +auto syntaxPayload = fuzzer.randomString(5uz, 20uz, "{}[]():;\"'\\/<>&="); +``` + +--- + +## Performance Measurements and Benchmarks + +The framework provides lightweight utilities to benchmark execution times and directly compare the performance of two implementations: + +```cpp +suite.test("FlatHashMap vs std::unordered_map performance") = [] { + constexpr auto elementCount = 50'000uz; + auto flatMap = CppUtils::Container::FlatHashMap{}; + auto stdMap = std::unordered_map{}; + + const auto flatBenchmark = benchmark("FlatHashMap insert", [&] { + for (auto index = 0uz; index < elementCount; ++index) + flatMap[index] = index * 2uz; + }); + + const auto stdMapBenchmark = benchmark("std::unordered_map insert", [&] { + for (auto index = 0uz; index < elementCount; ++index) + stdMap[index] = index * 2uz; + }); + + // Compare both benchmarks and report the speedup factor + const auto comparison = compareBenchmarks(flatBenchmark, stdMapBenchmark); + + // Non-blocking warning: logs an alert to the console if the candidate is slower + warnIfSlower(flatBenchmark, stdMapBenchmark); + + // If you instead wish to enforce a strict assertion that fails the test: + // expect(comparison.isFirstFaster()); +}; +``` + +--- + +## Parameterized Tests by Type and by Value (`forTypes` and `forValues`) + +The test framework supports iterative execution of identical test logic across a list of types or a set of values. These parameterization helpers apply seamlessly **on individual unit tests** as well as **at the complete suite level**. + + + +### Parameterization on an Individual Test + +#### Iterating Over a List of Types (`forTypes`) + +The test body is instantiated and registered as separate test cases (automatically suffixed with `[type 0]`, `[type 1]`, etc.) for each type specified in the template argument list: + +```cpp +suite.test("Vector operations").forTypes() = [] { + auto vector = std::vector{}; + vector.push_back(Type{}); +}; +``` + +Console report generated during test execution: + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Container/Vector/Vector operations [type 0]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Container/Vector/Vector operations [type 1]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Container/Vector/Vector operations [type 2]:[/blue] +[green]Passed ✓[/green] +``` + +#### Iterating Over a List of Values (`forValues`) + +The test creates a distinct entry for each supplied value. If the type is formattable via `std::format`, the value itself is displayed inside square brackets in the test label (`[10]`, `[50]`, etc.): + +```cpp +suite.test("Positive scale factor").forValues(10, 50, 100, 500) = [](int scale) { + expect(scale) > 0; +}; +``` + +Console report generated during test execution: + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Scale/Positive scale factor [10]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Scale/Positive scale factor [50]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Scale/Positive scale factor [100]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Scale/Positive scale factor [500]:[/blue] +[green]Passed ✓[/green] +``` + +#### Iterating Over a Container or Range (`std::ranges`) + +You can also pass a standard container or any `std::ranges` view (such as `std::views::iota` to test a lazy sequence of numbers without any heap allocations); each element generates an independent test execution: + +```cpp +// Lazy sequence with std::views::iota (zero dynamic allocations): +suite.test("Index validation").forValues(std::views::iota(1, 10)) = [](int index) { + expect(index) >= 1; + expect(index) < 10; +}; + +// Standard sequential container: +suite.test("Buffer allocation").forValues(std::vector{64uz, 256uz, 1024uz}) = [](std::size_t size) { + auto buffer = std::vector(size, 0); + expect(std::ranges::size(buffer)) == size; +}; + +// Associative container (iterates over each std::pair key-value entry): +const auto rolePermissions = std::map{ + {"guest", 1}, + {"user", 2}, + {"admin", 4} +}; +suite.test("Role permissions").forValues(rolePermissions) = [](const auto& entry) { + const auto& [role, permissionLevel] = entry; + expect(permissionLevel) > 0; +}; +``` + +### Parameterization on a Complete Suite + +The parameterization modifier also applies to registering entire suites (`testSuite(...).forTypes<...>()` or `testSuite(...).forValues(...)`). The runner automatically creates and registers an independent test suite per type (named, for instance, `"Container/Vector [type 0]"`, `"Container/Vector [type 1]"`) or per value (named `"Math/Prime [2]"`, `"Math/Prime [3]"`) : + +```cpp +auto _ = testSuite("Container/Vector").forTypes() = [](TestSuite& suite) { + suite.test("Default initialization") = [] { + auto container = std::vector{}; + expect(std::ranges::empty(container)); + }; +}; + +auto _ = testSuite("Math/Prime").forValues(2, 3, 5, 7) = [](TestSuite& suite, int prime) { + suite.test("Is prime") = [prime] { + expect(isPrime(prime)); + }; +}; +``` + +--- + +## Event Monitoring (`CppUtils::UnitTest::EventWatcher`) + +When testing event-driven architectures ([`CppUtils::Execution::EventQueue`](/en/reference/execution/eventqueue/), [`CppUtils::Execution::EventDispatcher`](/en/reference/execution/eventdispatcher/)), `CppUtils::UnitTest::EventWatcher` subscribes to an event upon construction and provides tools to record emission history and block until the expected number of occurrences is received without manual loops: + +```cpp +suite.test("Event dispatching") = [] { + using namespace std::chrono_literals; + + auto queue = CppUtils::Execution::EventQueue{}; + + // Create a typed event watcher on event "Notification" + auto watcher = watch<"Notification", int>(queue); + + expect(not watcher.hasOccurred()); + expect(watcher.count()) == 0uz; + + queue.emit<"Notification">(42); + queue.emit<"Notification">(84); + + // Wait actively until 2 occurrences arrive within 500ms + expect(watcher.waitForOccurrences(2uz, 500ms)); + expect(watcher.count()) == 2uz; + auto occurrences = watcher.occurrences(); + expect(occurrences->count) == 2uz; + expect(std::ranges::size(occurrences->payloads)) == 2uz; + expect(occurrences->payloads[0]) == 42; + expect(occurrences->payloads.back()) == 84; +}; +``` + +--- + +## Tracking Object Lifecycle: `CppUtils::UnitTest::LifetimeChecker` + +To verify the semantic correctness of your objects (containers, RAII wrappers, move-only types, zero-copy optimizations), CppUtils provides [`CppUtils::UnitTest::LifetimeChecker`](/en/reference/unittest/lifetimechecker/), an inert object designed to accurately track lifecycle operations (constructions, destructions, copies, and moves) and notify the caller. + +### Inspecting Copy and Move Counters + +The object maintains internal counters recording the number of copy and move operations performed: + +```cpp +import CppUtils; + +suite.test("Move semantics in vector") = [&] { + // Inert object with a no-op callback + auto checker = CppUtils::UnitTest::LifetimeChecker{[](std::string_view) {}, "Widget"}; + + auto container = std::vector{}; + container.reserve(1); + + // Move into container + container.push_back(std::move(checker)); + + expect(container.front().getCopyCount()) == 0uz; + expect(container.front().getMoveCount()) == 1uz; +}; +``` + + + +### Tracing Lifecycle Events + +The constructor accepts a callback function and an identifier tag. On every lifecycle operation (construction, copy, move, destruction), this callback is automatically invoked with a string describing the event: + +```cpp +suite.test("Lifecycle notifications") = [&] { + auto checker = CppUtils::UnitTest::LifetimeChecker{[](std::string_view event) { + std::println("{}", event); + }, "Resource"}; + + auto copy = checker; + auto moved = std::move(copy); +}; +``` + +The terminal output faithfully reflects the sequence of operations and the invalidation of the moved-from instance: + +```text +Resource() +Resource(const Resource&) +Resource(Resource&&) +~[invalid object]() +~Resource() +~Resource() +``` + +--- + +## Configuring and Executing Test Suites + +Invoking all registered suites is executed in the `main` entry point of your test runner binary via `executeTests()` : + +```cpp +import std; +import CppUtils; + +auto main(int argc, char** argv) -> int +{ + // Parse command-line options (mode, filter, verbose, failFast, chrono) + auto settings = CppUtils::UnitTest::executeCommands(argc, argv); + if (settings.abort) + return CppUtils::exitSuccess; + + return CppUtils::UnitTest::executeTests(settings); +} +``` + +### Configuration Options (`CppUtils::UnitTest::TestSettings`) + +| Option | Type | Default Value | Description | +| :--- | :--- | :--- | :--- | +| `verbose` | `bool` | `true` | Displays the name and real-time status of each test. If `false`, only prints the final summary. | +| `detail` | `bool` | `true` | Prints execution dividers and full suite namespaces during progress. | +| `chrono` | `bool` | `false` | Measures and logs the precise execution time of each test via `CppUtils::Log::ChronoLogger`. | +| `failFast` | `bool` | `false` | Immediately aborts test execution upon the first encountered failure. | +| `filter` | `std::string` | `""` | Filters test suites to execute by substring while automatically resolving their prerequisites. | +| `report` | `std::string` | `""` | Exports test results to a report file (format `junit:path.xml` or `path.xml`). | + +#### Command-Line Syntax + +The [`executeCommands()`](/en/reference/unittest/functions/#executeCommands) function parses binary arguments (`argc`, `argv`) according to POSIX and GNU conventions: + +```bash +# Standard long options +./UnitTests --filter=Network --chrono --report=junit:report.xml + +# Equivalent short options +./UnitTests -f=Network -c -r=report.xml + +# Immediate abort on first failure (fail-fast) +./UnitTests --fail-fast +``` + +| Long option | Short form | Default | Description | +| :--- | :--- | :--- | :--- | +| `--help` | `-h` | - | Shows available command-line parameters and terminates the program. | +| `--mode=` | `-m=` | `debug` | Predefined execution profile: `debug` (default), `test` (with timing), `prod` (silent with fail-fast). | +| `--verbose[=]` | `-v[=]` | `true` | Enables or disables standard execution logging. | +| `--detail[=]` | `-d[=]` | `true` | Enables or disables detailed progress output (test names, individual durations, benchmark metrics, separators). | +| `--fail-fast` | | `false` | Aborts test run immediately upon the first failure. | +| `--chrono[=]` | `-c[=]` | `false` | Displays individual test execution durations. | +| `--filter=` | `-f=` | `""` | Runs only test suites whose name contains this substring (and automatically resolves their dependencies). | +| `--report=` | `-r=` | `""` | Exports results in JUnit XML format (e.g., `-r=report.xml` or explicitly `--report=junit:report.xml`). | + +### JUnit XML Report Export and CI/CD Integration + +The `--report=junit:` (or simply `-r=`) option generates a report in universal **JUnit XML** format. Continuous integration engines process this standard format to display visual metrics and inject inline failure annotations directly on the offending lines of code: + +- **GitLab CI**: Supports JUnit XML **natively** (no plugin or third-party component required) via [`artifacts:reports:junit:`](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsjunit) in `.gitlab-ci.yml`. +- **GitHub Actions**: Does not natively parse test XML files by default. Rendering a graphical test summary and inline code annotations in Pull Requests is achieved using a dedicated GitHub marketplace action (such as [`EnricoMi/publish-unit-test-result-action`](https://github.com/EnricoMi/publish-unit-test-result-action) or [`dorny/test-reporter`](https://github.com/dorny/test-reporter)). +- **Jenkins**: Directly ingests the report using its official standard [JUnit Plugin](https://plugins.jenkins.io/junit/). + + + +```yaml +# .gitlab-ci.yml: Zero plugins required, native GitLab CI support +test: + stage: test + script: + - xmake run CppUtils-UnitTests --report=junit:build/test-results.xml + artifacts: + when: always + reports: + junit: build/test-results.xml +``` + + +```yaml +# .github/workflows/test.yml: Publishing via marketplace action + - name: Run unit tests + run: | + xmake run CppUtils-UnitTests --report=junit:build/test-results.xml + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: "build/test-results.xml" +``` + + + +### Process Isolation and Timeout Management + +Every unit test is executed in a dedicated child process. Memory access violations, fatal crashes, and infinite loops are cleanly isolated without interrupting the main test runner or affecting subsequent suites. + +This architecture provides two key guarantees: +- **Resilience to critical crashes**: If a test triggers a fatal signal (`SIGSEGV`, `SIGABRT`, etc.), the parent runner captures it. The test is recorded as failed with the signal's details, while the remaining tests continue execution normally. +- **Strict timeout termination**: If a test exceeds its allocated time limit (10 seconds by default, or customized via `.timeout(2s)`), it is terminated immediately, preventing test suite deadlocks. + +--- + +## Execution Report and Diagnostics + +### Successful Execution + +On a successful test run, the report displays structured progress: + +```terminal +2 tests found. Execution: +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Calculator/Addition:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Socket/TCP/Accept connection:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +Test results +All tests passed successfully +- 2 successful tests +``` + +### Targeted Failure and Dependency Cascade Handling + +When a foundational component fails, the report clearly isolates the failing line with evaluated values and automatically flags dependent suites as skipped: + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Socket/Init:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Socket/TCP:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Socket/TCP + In tests/Network/Socket.mpp +At line 48, column 3 +In expect(lhs) == rhs +with lhs: +500 +and rhs: +200 + +[red]Failed ✕[/red] +[blue]──────────────────────────────────────────────────────────────[/blue] +[yellow]Skipping test suite Network/Client due to failed prerequisites in test suite Network/Socket ⚠[/yellow] +[blue]──────────────────────────────────────────────────────────────[/blue] +Test results +The tests failed: +- 1 successful tests +- 1 failed tests + - Network/Socket +- 1 skipped tests + - Network/Client (due to failed prerequisites) +``` diff --git a/docs/src/content/docs/en/index.mdx b/docs/src/content/docs/en/index.mdx index 571517e8..df890b0e 100644 --- a/docs/src/content/docs/en/index.mdx +++ b/docs/src/content/docs/en/index.mdx @@ -16,21 +16,28 @@ hero: variant: minimal --- -import { Card, CardGrid } from '@astrojs/starlight/components'; +import { Card, CardGrid, LinkButton } from '@astrojs/starlight/components'; import { ContributorList } from 'astro-contributors'; export const base = import.meta.env.BASE_URL.replace(/\/$/, ''); ## Discover the documentation +CppUtils is structured around several major modular libraries (unit testing, networking, mesh topology) alongside a rich collection of specialized utilities. Modern C++20 modules allow you to import the entire suite at once (`import CppUtils;`) without inflating your final binary, as only the components actually consumed by your application are integrated at compile time. + - Step-by-step installation with XMake and Clang, then compiling your first modular C++26 program. + Step-by-step guide to setting up your environment and integrating CppUtils into your projects. + + + + + Modern C++ testing without macros: async assertions, fuzzing, benchmarks, and crash resilience. - + Practical guide to using `UniqueLocker` and `SharedLocker` to safely protect shared resources. @@ -41,6 +48,46 @@ export const base = import.meta.env.BASE_URL.replace(/\/$/, ''); +## Library Philosophy + +CppUtils is built upon uncompromising core principles: + + + Brings together core essentials (containers, concurrency, networking, parsing, unit tests, cryptography, etc.) to eliminate dependency proliferation. Powered by **C++ modules**, this breadth maximizes reusability with zero penalty: + - **Granular or all-in-one**: import a specific submodule (`import CppUtils.Container;`) or the entire suite (`import CppUtils;`). + - **Zero binary bloat**: only symbols actually invoked by your application are compiled into the final executable. + - **Hermetic isolation & faster builds**: each module is compiled only once, drastically reducing build times compared to redundant preprocessor (`#include`) headers. + + + + Zero architectural lock-in or contagion: CppUtils never attempts to dictate its types or colonize your codebase. + - **Non-invasive design & zero contagion**: unlike invasive frameworks such as **Qt** that mandate proprietary meta-compilers (`moc`), closed types (`QString`, `QByteArray`), and heavy inheritance trees, CppUtils remains strictly non-invasive and introduces zero viral dependencies: the goal is never to make you dependent to lock you into an ecosystem, actively refusing any form of vendor lock-in. + - **Design by contract**: APIs are constrained through standard concepts (`std::ranges::input_range`, `std::formattable`, `std::invocable`, etc.) rather than bound to rigid, imposed types: your containers propagate without defensive copying or conversion overhead (explore [design by contract with `auto` and concepts on my blog](https://morgancaron.github.io/articles/cpp/auto#abbreviated-function-template-depuis-c20)). + - **Isolated adoption**: pick a single standalone component (`MultiKeyMap`, `UniqueLocker`) in complete autonomy without being forced to import the rest of the library. + + + + Continuously targets the latest language standard (**C++26**) to immediately deliver state-of-the-art capabilities: native modules, monadic operations, advanced concepts, enhanced safety, and reflection. + - **Zero legacy compromise**: no artificial preservation of obsolete patterns or technical debt accumulation. + - **Pragmatic transition**: targeted feature-test macros (`__cpp_lib_*`) ease compilation on recent compilers with partial C++26 standard library rollouts, without carrying obsolete legacy baggage. + - **Future-proof architecture**: continuous refactorings to keep the codebase clean, robust, and modern. + + + + Maximum performance without ever sacrificing clarity or safety: + - **Readability & simplicity**: clean, transparent, and self-documenting APIs that remove accidental complexity without hidden magic. + - **Uncompromising performance**: strict zero-cost abstractions, cache-friendly memory layouts, and aggressive compile-time evaluation (`constexpr`, `consteval`). + - **Modern best practices**: systematic RAII resource management, strong typing enforced by concepts, and elimination of preprocessor macros (several of these [best practices are detailed on my blog](https://morgancaron.github.io/articles/cpp/)). + + +## Support the Project + +If you find CppUtils useful and wish to support its independent development, you can sponsor the project on GitHub Sponsors: + + + Sponsor on GitHub + + ## Contributors diff --git a/docs/src/content/docs/fr/guides/lockers.mdx b/docs/src/content/docs/fr/guides/lockers.mdx index 58b840e1..294335ff 100644 --- a/docs/src/content/docs/fr/guides/lockers.mdx +++ b/docs/src/content/docs/fr/guides/lockers.mdx @@ -1,8 +1,8 @@ --- -title: Synchronisation RAII (Lockers) +title: Verrous RAII description: Guide complet de synchronisation multithread, encapsulation, pièges et bonnes pratiques dans CppUtils::Thread. sidebar: - order: 3 + order: 4 --- import { Card, CardGrid, Tabs, TabItem, Aside } from '@astrojs/starlight/components'; @@ -208,6 +208,57 @@ auto account = CppUtils::Thread::UniqueLocker{}; } // Sortie de scope : libération unique du verrou, aucune rupture d'atomicité ``` +### Membres lockers et modificateur mutable : constance logique vs physique + +Lorsque vous intégrez un `UniqueLocker` ou un `SharedLocker` comme variable membre d'une classe, une question essentielle se pose : faut-il déclarer ce membre avec le modificateur `mutable` ? + +La réponse est **oui, dans la grande majorité des architectures concurrentes**, en raison de la distinction fondamentale entre constance logique et constance physique (binaire) : + +- **Constance logique** : Une méthode d'observation (par exemple `count() const`, `snapshot() const` ou `hasOccurred() const`) garantit à l'appelant qu'elle ne modifie pas l'état abstrait observable de l'objet hôte. +- **Constance physique (binaire)** : L'acquisition d'un verrou sous-jacent implique pourtant une modification physique d'un registre ou d'un flag atomique dans le mutex. + +#### Le cas concret : Synchronisation et variables de condition dans les méthodes const + +Considérons une classe de surveillance d'événements telle que `EventWatcher`. Ses méthodes d'interrogation et d'attente sont naturellement `const` du point de vue de l'appelant : + +```cpp showLineNumbers +class EventWatcher +{ + struct State + { + std::size_t count = 0; + std::vector payloads; + }; + + // Le modificateur mutable permet d'interagir avec le verrou depuis les méthodes const + mutable CppUtils::Thread::UniqueLocker m_state; + std::condition_variable m_condition; + +public: + [[nodiscard]] auto count() const noexcept -> std::size_t + { + return m_state.access()->count; + } + + auto waitForOccurrences(std::size_t target, std::chrono::milliseconds timeout) const -> bool + { + auto accessor = m_state.access(); + // std::condition_variable::wait_for nécessite un std::unique_lock actif et modifiable + return m_condition.wait_for(accessor.getLockGuard(), timeout, [&] { + return accessor->count >= target; + }); + } +}; +``` + +Sans le modificateur `mutable` sur `m_state` : +1. Dans une méthode qualifiée `const`, le compilateur considère `m_state` comme `const UniqueLocker`. +2. L'appel à `m_state.access()` sélectionne la surcharge `const&`, qui retourne un `Accessor`. +3. L'utilisation d'une primitive nécessitant la libération et la réacquisition atomique du verrou (comme `condition_variable::wait_for` avec `accessor.getLockGuard()`) échoue à la compilation car le verrou ne peut pas être manipulé de manière mutable. +4. Les mécanismes de mise en cache paresseuse (lazy initialization ou memoization thread-safe sous verrou) au sein d'une méthode de lecture `const` deviendraient impossibles sans rompre l'encapsulation. + +Déclarer les lockers membres avec `mutable` garantit ainsi que la responsabilité technique de la synchronisation thread-safe reste strictement découplée des garanties d'immuabilité logique offertes par vos méthodes `const`. + --- ## Accès partagé avec SharedLocker diff --git a/docs/src/content/docs/fr/guides/unit-test.mdx b/docs/src/content/docs/fr/guides/unit-test.mdx new file mode 100644 index 00000000..d17892db --- /dev/null +++ b/docs/src/content/docs/fr/guides/unit-test.mdx @@ -0,0 +1,1572 @@ +--- +title: Écrire des tests unitaires +description: Guide pratique pour concevoir, structurer et exécuter des tests unitaires fiables avec le module CppUtils::UnitTest sans dépendance externe. +sidebar: + order: 3 +--- + +import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components'; + +Les tests unitaires constituent le socle de toute base de code pérenne et évolutive. Pourtant, dans l'écosystème C++, leur mise en place impose historiquement de recourir à des bibliothèques tierces conçues il y a plus de quinze ans (GoogleTest, Catch2, doctest, Boost.Test, CppUnit). Bien qu'éprouvées, ces solutions historiques souffrent de compromis techniques majeurs : + +- **Prolifération des macros préprocesseur** : Les frameworks traditionnels imposent des macros aussi bien pour déclarer les suites et les cas de tests (`TEST`, `TEST_F`, `TEST_CASE`) que pour formuler chaque vérification (`ASSERT_EQ`, `CHECK`, etc.). Ces macros [ignorent le typage statique et les namespaces](https://morgancaron.github.io/articles/cpp/preprocessor#typage-et-namespaces), [polluent le namespace global](https://morgancaron.github.io/articles/cpp/preprocessor#pollution-namespace-global) et [entravent le débogage pas-à-pas](https://morgancaron.github.io/articles/cpp/preprocessor#debogage-pas-a-pas). Pourtant, le C++ permet d'assurer [l'enregistrement des suites de tests](#déclarer-une-suite-de-tests) et [le diagnostic des valeurs d'assertion](#diagnostic-des-valeurs) sans aucun artifice du préprocesseur. +- **Absence de modularité native** : Conçus pour le modèle historique d'inclusion d'en-têtes (`#include`), ces outils n'offrent pas d'intégration native avec les modules C++20, ce qui alourdit les temps de compilation. +- **Absence d'ordonnancement par dépendances et avalanche d'échecs** : Les frameworks traditionnels exécutent les tests sans aucune notion de dépendance réelle entre les composants. Si une brique fondamentale dysfonctionne (allocateur mémoire, socket réseau, file d'attente), tous les tests dépendants s'effondrent simultanément, noyant le terminal sous des dizaines d'échecs secondaires qui masquent la cause première. +- **L'illusion du mock comme palliatif** : Pour tenter d'isoler chaque composant, les développeurs multiplient les interfaces virtuelles et les objets factices (mocks). Ces simulations artificielles altèrent le comportement réel du système, alourdissent le code de production par des indirections inutiles (vtable, smart-pointers, allocations) et masquent les régressions d'intégration, les race conditions et les cas limites réels. + +Le module [`CppUtils::UnitTest`](/fr/reference/unittest/functions/) repense intégralement les tests unitaires pour l'ère moderne de C++26 : + + + + Syntaxe déclarative pure sans macro préprocesseur. Aucune injection de "magie" via la génération de code obscur par des macros. + + + Ordonnancement déterministe par un [graphe orienté acyclique (DAG)](https://fr.wikipedia.org/wiki/Graphe_orient%C3%A9_acyclique). L'exécution des tests se fait dans l'ordre de leurs dépendances, garantissant l'isolation de la cause racine sans recourir aux mocks. + + + Testez vos conditions avec les opérateurs naturels du langage (`==`, `!=`, `<`, etc.). En cas d'échec, les valeurs exactes comparées s'affichent automatiquement dans le rapport sans passer par des macros dédiées. + + + Chaque test s'exécute dans son propre sous-processus isolé ([`fork()`](#isolation-par-processus-et-gestion-du-temps-maximal)). Résilience absolue face aux crashs et aux boucles infinies (via watchdog temporel strict avec `SIGKILL`). + + + Vérifier qu'une exception précise survient bien dans certaines situations (via [`throws`](#matcher-throws--vérification-des-exceptions)). Ou par défaut, s'assurer qu'aucune n'est produite ([`noThrow`](#matcher-nothrow--absence-dexception)). Et ce, directement dans le test, mais également dans d'autres threads créés depuis ce test. + + + Exécuter un même scénario sur un éventail de types ([`forTypes`](#itération-sur-une-liste-de-types-fortypes)) ou une séquence de valeurs ([`forValues`](#itération-sur-une-liste-de-valeurs-forvalues)), que ce soit au niveau d'un test individuel ou à l'échelle d'une suite complète. + + + Ignorer proprement des tests lorsque l'environnement cible (système d'exploitation, ressources réseau, matériel spécifique) ne remplit pas les prérequis requis (via [`skip`](#ignorer-délibérément-un-test--skip-et-skipif) et [`skipIf`](#ignorer-délibérément-un-test--skip-et-skipif)), sans faire échouer la suite. + + + Génération pseudo-aléatoire déterministe pour tester des fonctions avec des entrées inattendues, pour pousser les cas testés dans leurs retranchements. Supporte également les chaînes de caractères aléatoires suivant des alphabets précis. + + + Mesures de performances intégrées au framework, avec warnings et suivi comparatif si la fonctionnalité testée n'atteint pas les standards de performance. + + + Vérification périodique d'une condition ([`eventually`](#matcher-eventually--attente-dune-condition-asynchrone)) ou attente d'une opération asynchrone ([`completes`](#matcher-completes--synchronisation-réactive-sans-polling)). + + + Validez vos contrats et calculs `constexpr` dès la compilation ([`suite.test`](#tests-à-la-compilation-suitetestpredicate)). Détection d'erreurs instantanée au build sans aucun surcoût d'exécution au runtime. + + + Génération de rapports standardisés JUnit XML ([`--report`](#export-de-rapports-junit-xml-et-intégration-cicd)). Intégration dans GitLab CI et GitHub Actions pour publier automatiquement les résultats et annotations dans vos pull requests. + + + +--- + +## Comparatif avec les frameworks de test en C++ + +`CppUtils::UnitTest` rompt avec les compromis des frameworks historiques : aucune macro préprocesseur, capture directe des expressions et ordonnancement automatique des suites par un graphe de dépendances. + +### Comparaison syntaxique + +Le comparatif interactif ci-dessous illustre un scénario de test identique exprimé dans chaque framework : + + + + + +```cpp +// Support natif des modules C++20 (zéro macro, zéro fichier d'en-tête) +import std; +import CppUtils; + +namespace MyApp::Tests +{ + namespace ut = CppUtils::UnitTest; + + // Déclaration de la suite avec dépendances + auto _ = ut::testSuite("Math/Calculator").dependsOn("Logger") = [](ut::TestSuite& suite) { + suite.test("Operations") = [] { + // Assertions avec capture des valeurs réelles sans macro + ut::expect(add(20, 22)) == 42; + not ut::expect(isNegative(42)); + ut::expect([&] { divide(10, 0); }) == ut::throws; + }; + + // Test paramétré sur une collection de valeurs + suite.test("Scale factor").forValues(10, 50, 100) = [](int scale) { + ut::expect(scale) > 0; + }; + }; +} +``` + + +```cpp +// Support natif des modules C++20 (zéro macro, zéro fichier d'en-tête) +import std; +import CppUtils; + +namespace MyApp::Tests +{ + using namespace CppUtils::UnitTest; + + // Déclaration de la suite avec dépendances + auto _ = testSuite("Math/Calculator").dependsOn("Logger") = [](TestSuite& suite) { + suite.test("Operations") = [] { + // Assertions avec capture des valeurs réelles sans macro + expect(add(20, 22)) == 42; + not expect(isNegative(42)); + expect([&] { divide(10, 0); }) == throws; + }; + + // Test paramétré sur une collection de valeurs + suite.test("Scale factor").forValues(10, 50, 100) = [](int scale) { + expect(scale) > 0; + }; + }; +} +``` + + + + +```cpp +import boost.ut; + +int main() { + // Pratique recommandée par la documentation de Boost.UT (un alias reste possible pour éviter les conflits de noms) + using namespace boost::ut; + + // Déclaration du test par un suffixe littéral + // Aucune gestion de dépendances entre suites (exécution linéaire) + "operations"_test = [] { + // Impose des suffixes littéraux (_i) pour capturer les valeurs en cas d'échec (restreint aux types arithmétiques basiques) + expect(add(20, 22) == 42_i); + expect(not isNegative(42)); + expect(throws([] { divide(10, 0); })); + }; + + // Test paramétré via surcharge de l'opérateur pipe | + "scale_factor"_test = [](int scale) { + expect(scale > 0_i); + } | std::vector{10, 50, 100}; +} +``` + + +```cpp +// Modules C++20 non supportés nativement (en-têtes #include obligatoires) +#include +#include +#include + +// Macros intrusives polluant le namespace global +// Aucune gestion de dépendances entre suites (exécution linéaire) +TEST_CASE("Math/Calculator Operations", "[math]") { + // Macros de préprocesseur obligatoires pour décomposer les opérandes + REQUIRE(add(20, 22) == 42); + REQUIRE_FALSE(isNegative(42)); + REQUIRE_THROWS_AS(divide(10, 0), std::invalid_argument); +} + +// Test paramétré : la macro GENERATE réexécute l'intégralité du corps du TEST_CASE +TEST_CASE("Math/Calculator Scale factor", "[math]") { + auto scale = GENERATE(10, 50, 100); + REQUIRE(scale > 0); +} +``` + + +```cpp +// Modules C++20 non supportés nativement (en-têtes #include obligatoires) +#include +#include + +// Macros intrusives polluant le namespace global +// Aucune gestion de dépendances entre suites (exécution linéaire) +TEST(CalculatorTest, Operations) { + EXPECT_EQ(add(20, 22), 42); // Macro distincte requise pour chaque opérateur + EXPECT_FALSE(isNegative(42)); + EXPECT_THROW(divide(10, 0), std::invalid_argument); +} + +// Test paramétré : impose une classe fixture dédiée dérivant de TestWithParam + 2 macros verbeuses +class ScaleTest : public testing::TestWithParam {}; + +TEST_P(ScaleTest, PositiveScale) { + EXPECT_GT(GetParam(), 0); +} + +INSTANTIATE_TEST_SUITE_P(Values, ScaleTest, testing::Values(10, 50, 100)); +``` + + +```cpp +// Modules C++20 non supportés nativement (en-têtes #include obligatoires) +#include +#include + +// Macros intrusives polluant le namespace global +// Aucune gestion de dépendances entre suites (exécution linéaire) +TEST_CASE("Math/Calculator") { + SUBCASE("Operations") { + CHECK(add(20, 22) == 42); // Décomposition via macros préprocesseur + CHECK_FALSE(isNegative(42)); + CHECK_THROWS_AS(divide(10, 0), std::invalid_argument); + } + + // Test paramétré : la macro GENERATE réexécute l'intégralité du corps du TEST_CASE (comme Catch2) + SUBCASE("Scale factor") { + auto scale = GENERATE(10, 50, 100); + CHECK(scale > 0); + } +} +``` + + +```cpp +// Modules C++20 non supportés nativement (en-têtes #include obligatoires) +#define BOOST_TEST_MODULE CalculatorTest +#include +#include +#include + +// Macros intrusives polluant le namespace global +// Aucune gestion de dépendances entre suites (exécution linéaire) +// Scope caché (namespace) injecté sans accolades visibles, enfermant tout code intermédiaire +BOOST_AUTO_TEST_SUITE(Calculator) + +BOOST_AUTO_TEST_CASE(Operations) { + BOOST_TEST(add(20, 22) == 42); + BOOST_TEST(!isNegative(42)); + BOOST_CHECK_THROW(divide(10, 0), std::invalid_argument); +} + +// Test paramétré : nécessite une macro de dataset lourde et complexe +BOOST_DATA_TEST_CASE(ScaleFactor, boost::unit_test::data::make({10, 50, 100}), scale) { + BOOST_TEST(scale > 0); +} + +BOOST_AUTO_TEST_SUITE_END() // Fermeture obligatoire du scope caché ouvert par la macro de suite +``` + + +```cpp +// Modules C++20 non supportés nativement (en-têtes #include obligatoires) +#include +#include + +// Déclaration verbeuse par classe dérivée de QObject +// Aucune gestion de dépendances entre suites (exécution linéaire) +class CalculatorTest : public QObject { + Q_OBJECT +private slots: + void testOperations() { + QCOMPARE(add(20, 22), 42); // Force les types à s'adapter aux tests en imposant de surcharger l'API propriétaire non standard QTest::toString pour le diagnostic + QVERIFY(!isNegative(42)); + QVERIFY_THROWS_EXCEPTION(std::invalid_argument, divide(10, 0)); + } + + // Test paramétré : impose 2 méthodes distinctes et un tableau dynamique de données + void testScale_data() { + QTest::addColumn("scale"); + QTest::newRow("10") << 10; + QTest::newRow("50") << 50; + QTest::newRow("100") << 100; + } + + void testScale() { + QFETCH(int, scale); + QVERIFY(scale > 0); + } +}; + +QTEST_MAIN(CalculatorTest) + +// Inclusion obligatoire du code généré par le MOC : dépendance à un préprocesseur propriétaire injectant du code invisible et appliquant une "magie" non standard ('Q_OBJECT', slots) pour introspecter les tests +#include "CalculatorTest.moc" +``` + + +```cpp +// Modules C++20 non supportés nativement (en-têtes #include obligatoires) +#include "CppUnitTest.h" +#include + +// Pratique officielle de Microsoft pour accéder à la classe Assert::, polluant volontairement le namespace global avec tout leur framework propriétaire +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +// Macros intrusives polluant le namespace global +// Aucune gestion de dépendances entre suites (exécution linéaire) +TEST_CLASS(CalculatorTest) { +public: + TEST_METHOD(TestOperations) { + Assert::AreEqual(42, add(20, 22)); + Assert::IsFalse(isNegative(42)); + Assert::ExpectException([] { divide(10, 0); }); + } + + // Test paramétré : aucun mécanisme natif, boucle impérative sans isolation + TEST_METHOD(TestScale) { + for (int scale : {10, 50, 100}) + Assert::IsTrue(scale > 0); + } +}; +``` + + +```cpp +// Modules C++20 non supportés nativement (en-têtes #include obligatoires) +#include +#include + +// Macros intrusives et classe fixture verbeuse +// Aucune gestion de dépendances entre suites (exécution linéaire) +class CalculatorTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(CalculatorTest); + CPPUNIT_TEST(testOperations); + CPPUNIT_TEST(testScale); + CPPUNIT_TEST_SUITE_END(); + +public: + void testOperations() { + CPPUNIT_ASSERT_EQUAL(42, add(20, 22)); // Macros d'assertions sans décomposition moderne + CPPUNIT_ASSERT(!isNegative(42)); + CPPUNIT_ASSERT_THROW(divide(10, 0), std::invalid_argument); + } + + // Test paramétré : aucun support natif, boucle impérative sans isolation + void testScale() { + for (int scale : {10, 50, 100}) + CPPUNIT_ASSERT(scale > 0); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(CalculatorTest); +``` + + + + + +### Tableau récapitulatif des fonctionnalités + +| Fonctionnalité | `CppUtils::UnitTest` | Boost.UT | Catch2 (v3) | Google Test | doctest | Boost.Test | Qt Test | MS Test (C++) | CppUnit | +| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| **Zéro macro de préprocesseur** | ✅ | ✅ | ❌ ([`REQUIRE`](https://github.com/catchorg/Catch2/blob/devel/docs/assertions.md)) | ❌ ([`EXPECT_EQ`](https://google.github.io/googletest/reference/assertions.html)) | ❌ ([`CHECK`](https://github.com/doctest/doctest/blob/master/doc/markdown/assertions.md)) | ❌ ([`BOOST_CHECK`](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/testing_tools/summary.html)) | ❌ ([`QCOMPARE`](https://doc.qt.io/qt-6/qtest.html#QCOMPARE)) | ❌ ([`TEST_METHOD`](https://learn.microsoft.com/visualstudio/test/microsoft-visualstudio-testtools-cppunittestframework-api-reference#test_macros)) | ❌ ([`CPPUNIT_`](https://freedesktop.org/wiki/Software/cppunit/)) | +| **Support natif des modules C++20** | ✅ (`import`) | ✅ ([`ut.cppm`](https://github.com/boost-ext/ut/blob/master/include/boost/ut.cppm)) | ❌ (en-têtes `#include`) | ❌ (en-têtes `#include`) | ❌ (en-têtes `#include`) | ❌ (en-têtes `#include`) | ❌ (en-têtes `#include`) | ❌ (en-têtes `#include`) | ❌ (en-têtes `#include`) | +| **Décomposition d'expressions (`lhs == rhs`)** | ✅ (syntaxe `expect(a) == b`) | ✅ (`1_i == 2` ou `that %` / `eq`) | ⚠️ Partiel (décomposition dans macros) | ❌ (macros dédiées [`ASSERT_EQ`](https://google.github.io/googletest/reference/assertions.html)) | ⚠️ Partiel (décomposition dans macros) | ⚠️ Partiel (via macro [`BOOST_TEST`](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/testing_tools/extended_comparison/test.html)) | ❌ ([`QCOMPARE`](https://doc.qt.io/qt-6/qtest.html#QCOMPARE)) | ❌ ([`Assert::AreEqual`](https://learn.microsoft.com/visualstudio/test/microsoft-visualstudio-testtools-cppunittestframework-api-reference#assert_classes)) | ❌ ([`CPPUNIT_ASSERT_EQUAL`](https://cppunit.sourceforge.net/doc/lastest/group___assertions.html#ga71162f02be052994ea26d15cb9115ab5)) | +| **Mock** | ❌ ([découvrir pourquoi](#pourquoi-aucun-utilitaire-de-mock-nest-fourni)) | ❌ | ❌ (via tiers) | ✅ ([GoogleMock](https://google.github.io/googletest/gmock_for_dummies.html)) | ❌ (via tiers) | ❌ (via tiers) | ⚠️ Partiel ([`QSignalSpy`](https://doc.qt.io/qt-6/qsignalspy.html)) | ❌ | ❌ | +| **Ordonnancement par dépendances** | ✅ (ordonnancement topologique et arrêt des branches défaillantes) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Benchmarks intégrés et comparatifs** | ✅ (`benchmark`, `warnIfSlower`) | ❌ | ⚠️ Partiel ([`BENCHMARK`](https://github.com/catchorg/Catch2/blob/devel/docs/benchmarks.md)) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Tests paramétrés (`forValues`)** | ✅ | ✅ | ✅ ([`GENERATE`](https://github.com/catchorg/Catch2/blob/devel/docs/generators.md)) | ✅ ([`INSTANTIATE_TEST_SUITE_P`](https://google.github.io/googletest/advanced.html#value-parameterized-tests)) | ✅ ([`GENERATE`](https://github.com/doctest/doctest/blob/master/doc/markdown/parameterized-tests.md)) | ✅ ([datasets](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/tests_organization/test_cases/test_case_generation.html)) | ⚠️ Partiel ([`QTest::newRow`](https://doc.qt.io/qt-6/qtest-overview.html#data-driven-testing)) | ❌ | ❌ | +| **Tests typés (`forTypes`)** | ✅ (direct via variadique) | ✅ | ✅ ([`TEMPLATE_TEST_CASE`](https://github.com/catchorg/Catch2/blob/devel/docs/test-cases-and-sections.md#type-parametrised-test-cases)) | ✅ ([`TYPED_TEST`](https://google.github.io/googletest/advanced.html#typed-tests)) | ⚠️ Partiel ([`TYPE_TO_STRING`](https://github.com/doctest/doctest/blob/master/doc/markdown/parameterized-tests.md)) | ✅ ([template test cases](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/tests_organization/test_cases/test_case_generation/datasets_auto_creation/types.html)) | ❌ | ❌ | ❌ | +| **Moteur de fuzzing déterministe** | ✅ (avec seed reproductible) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Isolation par processus (`fork`)** | ✅ (résilience `SIGSEGV`) | ❌ | ❌ | ⚠️ Partiel ([`death_test`](https://google.github.io/googletest/advanced.html#death-tests)) | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Vérification asynchrone (`eventually`, `completes`)** | ✅ (polling périodique et réveil réactif) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **Rapports JUnit XML (CI/CD)** | ✅ (natif avec `--report=junit:...`) | ❌ | ✅ ([`-r junit`](https://github.com/catchorg/Catch2/blob/devel/docs/reporters.md#junit-reporter)) | ✅ ([`--gtest_output=xml`](https://google.github.io/googletest/advanced.html#generating-an-xml-report)) | ✅ ([`-r=junit`](https://github.com/doctest/doctest/blob/master/doc/markdown/reporters.md#reporters-and-listeners)) | ✅ ([`--log_format=JUNIT`](https://www.boost.org/doc/libs/release/libs/test/doc/html/boost_test/utf_reference/rt_param_reference/log_format.html)) | ✅ ([`-junitxml`](https://doc.qt.io/qt-6/qtest-overview.html#qtest-command-line-arguments)) | ⚠️ Partiel ([`.trx` propriétaire](https://learn.microsoft.com/visualstudio/test/vstest-console-options), JUnit via plugin tiers) | ✅ ([`XmlOutputter`](https://cppunit.sourceforge.net/doc/lastest/class_xml_outputter.html)) | +| **Zéro dépendance externe** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ ([Boost](https://www.boost.org/)) | ❌ ([Qt Core & MOC](https://doc.qt.io/qt-6/qtest.html)) | ❌ ([Visual Studio](https://learn.microsoft.com/visualstudio/test/writing-unit-tests-for-c-cpp)) | ✅ | + +### Pourquoi cette architecture fait la différence + +- **Clarté du code et outillage moderne** : L'absence totale de macros permet une autocomplétion parfaite, un refactoring fiable dans votre éditeur de code, et un débogage pas-à-pas transparent sans code généré invisible. +- **Positionnement vis-à-vis des alternatives modernes** : Parmi l'ensemble des frameworks existants, **Boost.UT** constitue l'alternative la plus avancée et respectable pour s'affranchir des macros. `CppUtils::UnitTest` en conserve l'esprit moderne tout en éliminant les contraintes restantes : aucune exigence de littéraux dédiés (`_i`) pour capturer n'importe quel type standard, ordonnancement par graphe de dépendances et résilience par isolation étanche des processus. +- **Résolution immédiate de la cause racine** : Grâce au graphe de dépendances (`CppUtils::Container::DependencyGraph`), si un module fondamental de votre application échoue, les centaines de tests dépendants sont automatiquement ignorés au lieu d'inonder la console d'erreurs parasites. +- **Tout-en-un de la qualité logicielle** : Au lieu de jongler entre Google Test pour les tests unitaires, Google Benchmark pour les performances, libFuzzer pour la robustesse et des scripts maison pour l'isolation des processus, `CppUtils::UnitTest` rassemble ces piliers dans une interface cohérente et expressive. + +--- + +## Déclarer une suite de tests + +Une suite de tests se déclare simplement : + +```cpp +import std; +import CppUtils; + +namespace MyApp::Tests +{ + namespace ut = CppUtils::UnitTest; + + auto _ = ut::testSuite("Math/Calculator") = [](ut::TestSuite& suite) { + using namespace std::literals; + + suite.test("Addition") = [] { + const auto result = 20 + 22; + ut::expect(result) == 42; + }; + + suite.test("Calcul intensif").timeout(30s) = [] { + ut::expect(performHeavyComputation()) > 0; + }; + }; +} +``` + +### Dépendances entre suites de tests + +Lorsqu'une suite nécessite que d'autres suites de tests soient préalablement validées avant de s'exécuter, déclarez-les avec `.dependsOn(...)` : + +```cpp +auto _ = ut::testSuite("Network/Client").dependsOn("Logger", "Network/Socket") = [](ut::TestSuite& suite) { + suite.test("Connection") = [] { + // Ce test ne s'exécutera que si "Logger" et "Network/Socket" ont réussi + }; +}; +``` + +```mermaid +flowchart TD + subgraph Prerequisites["Suites prérequises"] + Logger["Logger"] + Socket["Network/Socket"] + end + + subgraph Dependent["Suite dépendante"] + Client["Network/Client"] + end + + Logger -->|"Succès requis"| Client + Socket -->|"Succès requis"| Client +``` + +En déclarant ces dépendances, l'exécuteur structure l'ordonnancement sous la forme d'un [graphe orienté acyclique (DAG)](https://fr.wikipedia.org/wiki/Graphe_orient%C3%A9_acyclique). Cette organisation apporte trois bénéfices majeurs : +- **Localisation immédiate de la cause racine d'un échec** : Si la suite `Network/Socket` échoue, la suite `Network/Client` n'est même pas lancée et est marquée comme ignorée en raison d'un prérequis non satisfait. Vous évitez ainsi de voir la cause réelle noyée sous une avalanche d'échecs en cascade provoqués par une dépendance défaillante. +- **Sortie console épurée avec le mode `failFast`** : Lorsque l'option [`failFast`](#options-de-configuration-cpputilsunittesttestsettings) est activée, l'exécuteur s'interrompt dès le premier échec sans exécuter les suites dépendantes. +- **Suppression du besoin de mocking** : Puisque les tests s'exécutent dans l'ordre strict de leurs dépendances réelles et que les pannes sont isolées à la source, il devient inutile d'introduire des doublures de test artificielles pour masquer les instabilités en amont. Vous pouvez tester directement vos composants réels de bout en bout en toute confiance. + +--- + +## Pourquoi aucun utilitaire de mock n'est fourni + +L'absence d'utilitaire de mock au sein de CppUtils est un choix délibéré. Dans l'écosystème C++, le recours aux mocks pose deux problèmes majeurs : + +### 1. L'illusion du test et le piège des fausses garanties + +Un mock remplace un composant authentique par des réponses pré-programmées. Ce procédé donne l'illusion de valider le composant, alors que l'interaction réelle n'a **jamais été exécutée** : +- **Exemple réseau** : Si l'on mocke une socket pour lui faire renvoyer un JSON figé, on valide le parsing dans des conditions idéales, mais on ignore comment notre composant réagit aux dynamiques réelles du flux : paquets fragmentés, désynchronisation d'état ou *race conditions* si des données arrivent pendant un changement d'état interne. Ces deux traitements peuvent alors entrer en conflit et corrompre le fonctionnement du composant, un défaut indétectable avec un mock. +- **Exemple filesystem** : Si l'on mocke le filesystem, on ignore les verrous concurrents, les droits d'accès réels du système d'exploitation et la synchronisation sur disque. + +En conditions réelles, le système se retrouve donc dans une situation qui n'a **véritablement jamais été testée**. CppUtils préconise de tester les interfaces en conditions authentiques : démarrer un vrai serveur local sur un port éphémère ou manipuler de vrais fichiers temporaires. + +### 2. Dénaturation du code de production (*Zero-cost abstraction*) + +Pour rendre une classe « mockable », le développeur est souvent contraint de dénaturer son architecture : imposer des hiérarchies orientées objet (POO) pour l'injection de dépendances, multiplier les interfaces virtuelles (`virtual` / vtable) et recourir aux allocations dynamiques (`std::unique_ptr`). Ces indirections empêchent l'inlining par le compilateur et alourdissent le code de production uniquement pour satisfaire le banc de tests. + +### L'alternative : ordonnancement par dépendances + +Le besoin de mocks naît souvent de la peur qu'une panne en amont ne fasse échouer tous les tests en cascade et qu'on ne sache plus isoler facilement la cause racine du problème. Grâce à [`CppUtils::Container::DependencyGraph`](/fr/reference/container/dependencygraph/), CppUtils résout ce problème : les briques de base [sont exécutées en premier](#dépendances-entre-suites-de-tests). Si une fondation échoue, les suites dépendantes sont automatiquement ignorées, isolant immédiatement la cause première sans introduire de simulation artificielle. + +--- + +## Écrire des assertions + +Contrairement aux frameworks traditionnels qui imposent de mémoriser des dizaines de macros spécialisées selon le type de vérification ([`ASSERT_EQ`](https://google.github.io/googletest/reference/assertions.html#EXPECT_EQ), [`ASSERT_NE`](https://google.github.io/googletest/reference/assertions.html#EXPECT_NE), [`ASSERT_THROW`](https://google.github.io/googletest/reference/assertions.html#EXPECT_THROW), [`CHECK_FALSE`](https://github.com/catchorg/Catch2/blob/devel/docs/assertions.md), etc. (voir [Comparatif](#comparatif-avec-les-frameworks-de-test-en-c))), `CppUtils::UnitTest` privilégie la simplicité en ne proposant **qu'une seule fonction d'assertion : `expect(...)`**. + +### Assertion booléenne : `expect` + +La fonction `expect` valide qu'une condition ou une expression est vraie : + +```cpp +ut::expect(database.isConnected()); +ut::expect(not std::ranges::empty(buffer)); +``` + +Écrire `== true` est inutile : `expect(condition);` évalue directement la condition. + +En cas d'échec, le rapport affiche précisément le fichier, la ligne, la colonne ainsi que la valeur textuelle défaillante : + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Database/Connection/Ping:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Database/Connection/Ping + In tests/Database/Connection.mpp +At line 42, column 5 +In expect(condition) +with value: +[red]false[/red] + +[red]Failed ✕[/red] +``` + +#### Objets convertibles en booléen + +Tout type convertible en booléen (`std::optional`, pointeurs bruts, `std::unique_ptr`, `std::shared_ptr`, types avec `explicit operator bool()`) est automatiquement converti et évalué sous forme de booléen par `expect` : + +```cpp +auto user = std::optional{fetchUser(42)}; +ut::expect(user); + +auto connection = std::unique_ptr{connect()}; +ut::expect(connection); +``` + +En cas d'échec sur un pointeur nul ou un `std::optional` vide, le rapport affiche directement `with value: nullptr` ou `with value: nullopt` en rouge : + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Database/Connection/Open:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Database/Connection/Open + In tests/Database/Connection.mpp +At line 42, column 2 +In expect(condition) +with value: +[red]nullopt[/red] + +[red]Failed ✕[/red] +``` + +#### Négation : `not expect(...)` plutôt que `expect(not ...)` + +Pour affirmer qu'une condition doit être fausse, préférez placer la négation à l'extérieur : + +```cpp +// Recommandé : +not expect(session.isExpired()); + +// Équivalent : +expect(session.isExpired()) == false; +``` + +Bien qu'écrire `expect(not condition);` fonctionne en C++, la négation externe offre un diagnostic bien plus riche en cas d'échec : + +- Avec `expect(not error);` : la négation logique est résolue *avant* l'appel à `expect`. L'expression est immédiatement réduite à un simple `bool`. En cas d'échec, le rapport affiche uniquement `with value: false` sans révéler le contenu de l'objet : + +```cpp +auto error = std::optional{"Connexion refusée"}; +expect(not error); +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Connection:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Connection + In tests/Network/Connection.mpp +At line 42, column 5 +In expect(condition) +with value: +[red]false[/red] + +[red]Failed ✕[/red] +``` + +- Avec `not expect(error);` : le framework capture l'objet d'origine intact. Pour tout objet déréférençable (`operator*`) dont la valeur sous-jacente est affichable, le diagnostic extrait et affiche directement l'élément inattendu qui a provoqué l'erreur : + +```cpp +auto error = std::optional{"Connexion refusée"}; +not expect(error); +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Connection:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Connection + In tests/Network/Connection.mpp +At line 42, column 5 +In not expect(condition) +with value: +[red]Connexion refusée[/red] + +[red]Failed ✕[/red] +``` + +### Diagnostic des valeurs + +Dans les bibliothèques traditionnelles, inspecter la valeur des opérandes impose de renoncer aux opérateurs C++ au profit d'une longue liste de macros spécialisées à mémoriser ([`ASSERT_EQ`](https://google.github.io/googletest/reference/assertions.html#EXPECT_EQ), [`ASSERT_NE`](https://google.github.io/googletest/reference/assertions.html#EXPECT_NE), [`ASSERT_LT`](https://google.github.io/googletest/reference/assertions.html#EXPECT_LT), [`ASSERT_LE`](https://google.github.io/googletest/reference/assertions.html#EXPECT_LE), [`ASSERT_GT`](https://google.github.io/googletest/reference/assertions.html#EXPECT_GT), [`ASSERT_GE`](https://google.github.io/googletest/reference/assertions.html#EXPECT_GE), [`EXPECT_FLOAT_EQ`](https://google.github.io/googletest/reference/assertions.html#EXPECT_FLOAT_EQ), etc.). + +Avec `CppUtils::UnitTest`, les opérateurs standards du C++ remplacent l'intégralité de ces macros, sans wrapper verbeux ni [littéral propriétaire](https://boost-ext.github.io/ut/) : + +```cpp +ut::expect(left) == right; +ut::expect(left) != right; +ut::expect(left) < right; +ut::expect(left) <= right; +ut::expect(left) > right; +ut::expect(left) >= right; +``` + +```cpp +namespace ut = CppUtils::UnitTest; + +auto _ = ut::testSuite("Auth/Session") = [](ut::TestSuite& suite) { + using namespace std::chrono_literals; + + suite.test("Validate session") = [] { + ut::expect(session.getRemainingTime()) > 0s; + ut::expect(session.getUserRole()) == "admin"; + ut::expect(session.getFailedAttempts()) <= 3u; + ut::expect(std::ranges::size(session.getActiveTokens())) == 2uz; + }; +}; +``` + +#### Pourquoi cette syntaxe a été choisie + +1. **Pur C++23 sans macro préprocesseur** : Dans des frameworks comme Catch2 ou GoogleTest, la décomposition d'expression nécessite impérativement une macro (`REQUIRE(a == b)`, `EXPECT_EQ(a, b)`). En C++ pur, écrire `expect(a == b);` transmettrait un simple `bool` déjà évalué à `expect`, rendant impossible l'inspection séparée de `a` et `b`. En écrivant `expect(a) == b;`, `expect(a)` renvoie un objet proxy temporaire (`ExpectationProxy`) qui intercepte l'opérateur de comparaison suivant et capture à la fois la valeur de gauche (`lhs`) et celle de droite (`rhs`) avec leur opérateur sans la moindre macro. +2. **Zéro conflit d'identifiants et zéro masquage de variable** : Les approches basées sur des fonctions libres comme `value(...)` ou `that(...)` entraînent des conflits majeurs dès qu'un test déclare une variable locale portant ce même nom usuel. Écrire `auto value = calculate(); expect(value(value) == 42);` produit une erreur fatale à la compilation (`error: type does not provide a call operator`). Avec `expect(value) == 42;`, la variable `value` est passée directement sans aucun risque d'écrasement ou d'ambiguïté. +3. **Zéro pollution par des littéraux de test personnalisés** : Pour intercepter les comparaisons sans macro, [Boost.UT](https://boost-ext.github.io/ut/) doit forcer au moins l'un des opérandes dans un type intermédiaire : soit via des suffixes littéraux propriétaires (`42_i == sum()`), restreints aux types arithmétiques primitifs, soit via un wrapper de matcher (`expect(that % 0 == sum());`), soit via des fonctions préfixées (`expect(eq(a, b));`). `CppUtils::UnitTest` résout cette problématique de manière unifiée et générique sans aucun compromis : aucun littéral utilisateur imposé, aucun wrapper verbeux et aucune fonction préfixée. N'importe quel type C++ moderne est immédiatement capturé avec les opérateurs naturels du langage. +4. **Lisibilité directe** : L'assertion se lit de gauche à droite sans rupture ("expect left equals right") et sans préfixes redondants. +5. **Assertions sans opérateur relationnel** : Les conditions booléennes s'évaluent directement (par exemple `ut::expect(user.isConnected());`) sans exiger d'opérateur de comparaison ni de suffixe `== true`. + +En cas d'échec (par exemple si `user.getRole()` renvoie `"guest"` au lieu de `"admin"`), le diagnostic décompose l'expression et affiche les valeurs exactes des deux opérandes : + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]UnitTest/Auth:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Auth/Session/Admin verification + In tests/Auth/Session.mpp +At line 42, column 2 +In expect(lhs) == rhs +with lhs: +guest +and rhs: +admin + +Diff: + lhs (-): "[red]guest[/red]" + rhs (+): "[green]admin[/green]" + +[red]Failed ✕[/red] +``` + + + +Lors de divergences ponctuelles au sein de longues chaînes, ce calcul de diff isole instantanément les caractères fautifs au milieu du texte identique : + +```cpp +const auto expectedUrl = "https://api.example.com/v1/users/42/profile"; +const auto actualUrl = "https://api.example.com/v2/users/42/profile"; +ut::expect(expectedUrl) == actualUrl; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Api/EndpointCheck:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Api/EndpointCheck + In tests/Network/Api.mpp +At line 18, column 2 +In expect(lhs) == rhs +with lhs: +https://api.example.com/v1/users/42/profile +and rhs: +https://api.example.com/v2/users/42/profile + +Diff: + lhs (-): "https://api.example.com/v[red]1[/red]/users/42/profile" + rhs (+): "https://api.example.com/v[green]2[/green]/users/42/profile" + +[red]Failed ✕[/red] +``` + +*(Comme affiché ci-dessus, seul le chiffre `1` apparaît en rouge et le `2` en vert, tout le reste de l'URL restant en couleur neutre).* + +De même, lorsque plusieurs segments diffèrent au sein d'une même phrase, chaque divergence est mise en évidence individuellement sans amalgamer le texte intermédiaire inchangé : + +```cpp +const auto expectedSentence = "The quick brown fox jumps over the lazy dog"; +const auto actualSentence = "The fast brown fox leaps over a lazy dog"; +ut::expect(expectedSentence) == actualSentence; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]UnitTest/VisualColorInspection/Diff visuel complexe (segments multiples):[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +UnitTest/VisualColorInspection/Diff visuel complexe (segments multiples) + In tests/UnitTest/UnitTest.mpp +At line 711, column 5 +In expect(lhs) == rhs +with lhs: +The quick brown fox jumps over the lazy dog +and rhs: +The fast brown fox leaps over a lazy dog + +Diff: + lhs (-): "The [red]quick[/red] brown fox [red]jum[/red]ps over [red]the[/red] lazy dog" + rhs (+): "The [green]fast[/green] brown fox [green]lea[/green]ps over [green]a[/green] lazy dog" + +[red]Failed ✕[/red] +``` + +### Prise en charge native des types complexes + +Le moteur d'inspection s'appuie sur des concepts C++20 génériques, sans exiger de macro de sérialisation ni de surcharge d'affichage (contrairement par exemple à [`QTest::toString`](https://doc.qt.io/qt-6/qtest.html#toString) dans Qt Test). Tout type qui satisfait ces concepts (plages itérables, types déréférençables, structures décomposables façon tuple, etc.), qu'il provienne de la bibliothèque standard, d'une bibliothèque tierce ou de votre propre code, est automatiquement analysé et mis en forme. + +#### Conteneurs et plages (*ranges*) + +Les conteneurs (`std::vector`, `std::span`, listes, vues) sont comparés élément par élément avec `std::ranges::equal` et formatés sous forme de liste lisible : + +```cpp +const auto headers = std::vector{"content-type", "authorization"}; +ut::expect(headers) == std::vector{"content-type", "accept"}; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Headers/Validate:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Headers/Validate + In tests/Network/Headers.mpp +At line 32, column 2 +In expect(lhs) == rhs +with lhs: +["content-type", "authorization"] +and rhs: +["content-type", "accept"] + +Diff: + lhs (-): ["content-type", "[red]authorization[/red]"] + rhs (+): ["content-type", "[green]accept[/green]"] + +[red]Failed ✕[/red] +``` + +#### Tuples et paires + +Les structures composites `std::tuple` et `std::pair` sont décomposées et formatées récursivement champ par champ : + +```cpp +const auto response = std::make_pair(404, "Not Found"); +ut::expect(response) == std::make_pair(200, "OK"); +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Response/Validate:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Response/Validate + In tests/Network/Response.mpp +At line 15, column 2 +In expect(lhs) == rhs +with lhs: +(404, "Not Found") +and rhs: +(200, "OK") + +Diff: + lhs (-): ([red]404[/red], "[red]Not Found[/red]") + rhs (+): ([green]200[/green], "[green]OK[/green]") + +[red]Failed ✕[/red] +``` + +#### Types déréférençables et optionnels + +`std::optional`, `std::unique_ptr` et `std::shared_ptr` sont inspectés automatiquement : ils affichent explicitement `nullopt` ou `nullptr` lorsqu'ils sont vides, ou la valeur pointée déréférencée : + +```cpp +const auto cachedUser = std::optional{std::nullopt}; +ut::expect(cachedUser) == std::optional{"alice"}; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Auth/Cache/Lookup:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Auth/Cache/Lookup + In tests/Auth/Cache.mpp +At line 24, column 2 +In expect(lhs) == rhs +with lhs: +[red]nullopt[/red] +and rhs: +"alice" + +[red]Failed ✕[/red] +``` + +#### Octets mémoire (`std::byte`) et buffers binaires + +Les octets mémoire sont mis en forme sous notation hexadécimale explicite (`0x2A`), y compris lorsqu'ils sont combinés dans des collections (`std::vector`, `std::span`), idéal pour tester des protocoles binaires et des en-têtes réseau : + +```cpp +const auto header = std::vector{std::byte{0x7F}, std::byte{0x45}, std::byte{0x4C}, std::byte{0x46}}; // ELF magic +ut::expect(header) == std::vector{std::byte{0x7F}, std::byte{0x45}, std::byte{0x00}, std::byte{0x00}}; +``` + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Binary/Header/Magic:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Binary/Header/Magic + In tests/Binary/Header.mpp +At line 12, column 2 +In expect(lhs) == rhs +with lhs: +[0x7F, 0x45, 0x4C, 0x46] +and rhs: +[0x7F, 0x45, 0x00, 0x00] + +Diff: + lhs (-): [0x7F, 0x45, [red]0x4C[/red], [red]0x46[/red]] + rhs (+): [0x7F, 0x45, [green]0x00[/green], [green]0x00[/green]] + +[red]Failed ✕[/red] +``` + +#### Chemins de fichiers (`std::filesystem::path`) + +Les chemins de fichiers sont normalisés et affichés sous leur forme canonique : + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]FileSystem/Config/PathCheck:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +FileSystem/Config/PathCheck + In tests/FileSystem/Config.mpp +At line 28, column 2 +In expect(lhs) == rhs +with lhs: +"/var/log/syslog" +and rhs: +"/var/log/app.log" + +Diff: + lhs (-): "/var/log/[red]sys[/red]log" + rhs (+): "/var/log/[green]app.[/green]log" + +[red]Failed ✕[/red] +``` + +#### Nombres flottants avec tolérance epsilon automatique + +Les comparaisons de nombres à virgule flottante (`float`, `double`) s'appuient automatiquement sur une tolérance relative epsilon ([`Math::isEqual`](/fr/reference/math/functions/#isEqual)), éliminant les échecs liés aux arrondis binaires IEEE 754 sans imposer de macro dédiée ([`EXPECT_NEAR`](https://google.github.io/googletest/reference/assertions.html#EXPECT_NEAR)) : + +```cpp +ut::expect(0.1 + 0.2) == 0.3; // Valide automatiquement malgré l'imprécision IEEE 754 +``` + +#### Entiers hétérogènes (signés / non signés) + +Les comparaisons mixtes s'appuient sur les fonctions sécurisées de la bibliothèque standard (`std::cmp_equal`, `std::cmp_greater`, etc.), éliminant les faux positifs dus aux conversions implicites risquées du C++ : + +```cpp +ut::expect(-1) > 1u; // Faux en C++ standard à cause de la conversion non signée ; évalué correctement ici +``` + +#### Chaînes de caractères et littéraux + +La comparaison avec un littéral textuel compare le contenu des chaînes via `std::string_view`, éliminant le risque classique de comparer par inadvertance les adresses mémoire des pointeurs `const char*`. + +### Matchers : `throws`, `noThrow`, `crashes`, `eventually`, `completes` + +Plutôt que d'invoquer des fonctions d'assertion verbeuses et dispersées ([`ASSERT_THROW`](https://google.github.io/googletest/reference/assertions.html#EXPECT_THROW), [`ASSERT_NO_THROW`](https://google.github.io/googletest/reference/assertions.html#EXPECT_NO_THROW), [`ASSERT_DEATH`](https://google.github.io/googletest/reference/assertions.html#death-assertions), etc.), `CppUtils::UnitTest` unifie les vérifications via l'opérateur `==` appliqué à `expect(action)` : + +#### Matcher `throws` : Vérification des exceptions + +Validation du type d'exception : + +```cpp +ut::expect([&] { + computeSquareRoot(-1.0); +}) == ut::throws; +``` + +Validation que le message d'erreur contient une sous-chaîne : + +```cpp +ut::expect([&] { + divide(10, 0); +}) == ut::throws("Division par zéro"); +// Valide si exception.what() contient "Division par zéro" (ex. "Erreur mathématique: Division par zéro détectée") +``` + +Validation par prédicat booléen : + +```cpp +ut::expect([&] { + database.connect("invalid_host"); +}) == ut::throws([](const DatabaseException& error) { + return error.code == 404 and error.message.contains("unreachable"); +}); +``` + +Validation par comparaison directe de valeur : + +```cpp +ut::expect([&] { + parser.parse("!"); +}) == ut::throws(ParseError{.line = 1, .code = ErrorCode::UnexpectedToken}); +``` + +Validation par inspecteur avec assertions internes : + +```cpp +ut::expect([&] { + database.connect("invalid_host"); +}) == ut::throws([&](const DatabaseException& error) { + ut::expect(error.code) == 404; + ut::expect(error.message.contains("unreachable")); +}); +``` + +#### Matcher `noThrow` : Absence d'exception + +Vérifie qu'aucune exception n'est lancée lors de l'exécution : + +```cpp +ut::expect([&] { + socket.receive(); +}) == ut::noThrow; +``` + +#### Matcher `crashes` : Interception de crashs et signaux critiques + +Validation d'une interruption fatale (tout signal d'arrêt critique) : + +```cpp +ut::expect([&] { + int* nullPointer = nullptr; + *nullPointer = 42; +}) == ut::crashes; +``` + +Validation d'un signal fatal spécifique : + +```cpp +ut::expect([&] { + std::abort(); +}) == ut::crashes(CppUtils::System::Signal::Abort); +``` + +#### Matcher `eventually` : Attente d'une condition asynchrone + +Vérifie périodiquement qu'une condition asynchrone devient vraie avant un délai maximal. + +Par défaut, `eventually` attend jusqu'à **1 seconde** en réévaluant la condition toutes les **20 ms** : + +```cpp +// Utilisation avec les valeurs par défaut (timeout: 1s, step: 20ms) +ut::expect([&] { + return counter.load() == 5uz; +}) == ut::eventually; +``` + +Il est également possible de personnaliser le délai maximal seul, ou conjointement avec l'intervalle périodique : + +```cpp +// Délai maximal personnalisé de 500ms (intervalle conservé à 20ms) +ut::expect([&] { + return isReady.load(); +}) == ut::eventually(500ms); + +// Délai maximal (500ms) et intervalle d'évaluation (10ms) personnalisés +ut::expect([&] { + return counter.load() == 5uz; +}) == ut::eventually(500ms, 10ms); +``` + +#### Matcher `completes` : Synchronisation réactive sans polling + +Attend la fin d'une opération asynchrone ou le déblocage d'une primitive de synchronisation avant un délai maximal (1 seconde par défaut). + +Attente d'un `std::future` : + +```cpp +auto promise = std::promise{}; +auto future = promise.get_future(); +worker.execute([&] { promise.set_value(42); }); + +// Validation de la complétion seule +ut::expect(future) == ut::completes; + +// Validation combinée de la complétion et de la valeur +ut::expect(future) == ut::completes.with(42); + +// Personnalisation du délai maximal +ut::expect(future) == ut::completes(500ms).with(42); +``` + +Avec une lambda retournant un résultat asynchrone : + +```cpp +ut::expect([&] { return worker.computeAsync(); }) == ut::completes.with(42); +``` + +Avec un `std::shared_future` : + +```cpp +auto sharedFuture = promise.get_future().share(); +ut::expect(sharedFuture) == ut::completes.with(42); +``` + +Avec un sémaphore C++20 (`std::binary_semaphore`, `std::counting_semaphore`) : + +```cpp +auto signal = std::binary_semaphore{0}; +scheduler.schedule([&] { signal.release(); }, 50ms); + +ut::expect(signal) == ut::completes; +``` + +--- + + + +### Ignorer délibérément un test : `skip` et `skipIf` + +Lorsqu'un test est temporairement désactivé ou requiert un environnement spécifique (système d'exploitation particulier, ressource matérielle requise, connectivité réseau distante, etc.), vous pouvez l'ignorer proprement sans faire échouer la suite : + +```cpp +// Ignorer inconditionnellement un test (ex. fonctionnalité en cours de développement ou bogue en attente de correctif) +skip("Fonctionnalité en cours de refactorisation (ticket #142)"); + +// Ignorer un test selon une condition +skipIf(not isNetworkAvailable(), "Connexion réseau distante requise"); +``` + +--- + +## Tests à la compilation (`suite.test`) + +Pour valider vos algorithmes `constexpr`, métaprogrammation par templates, concepts ou constantes statiques, le framework de test fournit une surcharge directe de `suite.test` évaluée à la compilation : + +```cpp +suite.test<[] { + constexpr auto value = factorial(5); + return value == 120; +}>("Factorial computation"); +``` + +Ce mécanisme combine la rigueur d'un `static_assert` avec l'intégration complète au banc de tests : +- **Feedback statique immédiat sans réexécution** : Si le prédicat échoue, la compilation échoue immédiatement avec une erreur `static_assert`, bloquant le binaire avant toute exécution. Vous savez que le test passe dès que le projet compile, même sans avoir à relancer la suite de tests. +- **Intégration au rapport et statistiques** : Le test compile-time est comptabilisé dans le bilan de la suite et affiche son statut (`Passed ✓`). + + + +--- + +## Tests de robustesse et fuzzing (`fuzzTest` et `fuzz`) + +Le framework intègre un moteur de fuzzing pseudo-aléatoire déterministe. Ce déterminisme garantit que chaque exécution de test rejoue rigoureusement la même séquence de données pour une reproductibilité absolue en intégration continue : + +```cpp +// Exécute 5 000 itérations avec des valeurs régénérées pseudo-aléatoirement à chaque passage +suite.fuzzTest("Parser fuzzing", 5'000uz) = [](auto& fuzzer) { + // Génère une chaîne pseudo-aléatoire de longueur 1 à 64 caractères + const auto input = fuzzer.randomString(1uz, 64uz); + + // Valide que le parser ne subit aucun comportement indéfini ni plantage + parseData(input); +}; +``` + +Vous pouvez également injecter des séquences pseudo-aléatoires au sein d'un test unitaire classique à l'aide de `fuzz` : + +```cpp +suite.test("Integer arithmetic fuzzing") = [] { + fuzz("Random arithmetic checks", 1'000uz, [](auto& fuzzer) { + const auto a = fuzzer.random(-1000, 1000); + // Borne inférieure à 1 pour exclure toute division par zéro + const auto b = fuzzer.random(1, 1000); + expect((a / b) * b + (a % b)) == a; + }); +}; +``` + +### Générateurs pseudo-aléatoires disponibles + +L'objet `fuzzer` (instance de [`CppUtils::Math::DeterministicRandomEngine`](/fr/reference/math/deterministicrandomengine/)) met à disposition un ensemble complet de méthodes de génération déterministes entièrement compatibles `constexpr` : + +| Méthode & signature | Description | +| :--- | :--- | +| [`random()`](/fr/reference/math/deterministicrandomengine/#random_2) | Génère une valeur pseudo-aléatoire du type demandé (`int`, `size_t`, `double`, `bool`, etc.) couvrant par défaut les bornes minimales et maximales du type. | +| [`random(min, max)`](/fr/reference/math/deterministicrandomengine/#random) | Génère une valeur pseudo-aléatoire bornée dans l'intervalle `[min, max]` avec déduction automatique du type (entier ou flottant). | +| [`randomSize(max)`](/fr/reference/math/deterministicrandomengine/#randomSize_2) | Raccourci pratique pour générer une taille `std::size_t` comprise entre `0` et `max` inclus (ou dans `[min, max]` avec 2 arguments). Sans argument, génère une taille bornée par défaut par `PTRDIFF_MAX` [conformément à la limite légale des objets contigus en C++](https://morgancaron.github.io/articles/cpp/size#limitation-de-la-plage-de-valeurs). | +| [`randomFloat(min, max)`](/fr/reference/math/deterministicrandomengine/#randomFloat) | Génère un flottant dans l'intervalle `[min, max]` (par défaut `0.0` à `1.0`). | +| [`randomString(minLen, maxLen, alphabet)`](/fr/reference/math/deterministicrandomengine/#randomString) | Génère une chaîne de longueur variable (par défaut 0 à 32 caractères) issue de l'alphabet fourni. | +| [`randomBytes(count)`](/fr/reference/math/deterministicrandomengine/#randomBytes) | Alloue et remplit un vecteur de `count` octets (`std::vector`). | +| [`pickOne(collection)`](/fr/reference/math/deterministicrandomengine/#pickOne) | Sélectionne un élément aléatoire au sein d'une collection. | + +Pour le détail exhaustif de leurs paramètres et surcharges, consultez la [référence d'API de `DeterministicRandomEngine`](/fr/reference/math/deterministicrandomengine/). + +### Personnalisation des chaînes aléatoires + +Le paramètre `alphabet` de `randomString` permet de cibler des motifs et formats spécifiques : + +```cpp +// Longueur fixe de 32 caractères (min=32, max=32) tirés de l'alphabet hexadécimal (hash, token UUID) +auto hexToken = fuzzer.randomString(32uz, 32uz, "0123456789abcdef"); + +// Longueur variable de 1 à 10 caractères restreinte aux seuls chiffres 0 à 9 +auto numericId = fuzzer.randomString(1uz, 10uz, "0123456789"); + +// Longueur variable de 5 à 20 caractères spéciaux pour éprouver la robustesse d'un parseur +auto syntaxPayload = fuzzer.randomString(5uz, 20uz, "{}[]():;\"'\\/<>&="); +``` + +--- + +## Mesures de performances et benchmarks + +Le framework met à disposition des utilitaires légers pour chronométrer votre code et comparer directement les performances de deux implémentations : + +```cpp +suite.test("FlatHashMap vs std::unordered_map performance") = [] { + constexpr auto elementCount = 50'000uz; + auto flatMap = CppUtils::Container::FlatHashMap{}; + auto stdMap = std::unordered_map{}; + + const auto flatBenchmark = benchmark("FlatHashMap insert", [&] { + for (auto index = 0uz; index < elementCount; ++index) + flatMap[index] = index * 2uz; + }); + + const auto stdMapBenchmark = benchmark("std::unordered_map insert", [&] { + for (auto index = 0uz; index < elementCount; ++index) + stdMap[index] = index * 2uz; + }); + + // Compare les deux benchmarks et rapporte le facteur d'accélération + const auto comparison = compareBenchmarks(flatBenchmark, stdMapBenchmark); + + // Avertissement non bloquant : affiche une alerte dans la console si le candidat est plus lent + warnIfSlower(flatBenchmark, stdMapBenchmark); + + // Si vous souhaitez au contraire imposer une contrainte stricte qui fait échouer le test : + // expect(comparison.isFirstFaster()); +}; +``` + +--- + +## Tests paramétrés par types et par valeurs (`forTypes` et `forValues`) + +Le framework de test prend en charge l'exécution itérative d'un même scénario de test pour une liste de types ou un jeu de valeurs distinctes. Ces modificateurs s'appliquent aussi bien **au niveau d'un test unitaire isolé** qu'**au niveau d'une suite entière**. + + + +### Paramétrage au niveau d'un test individuel + +#### Itération sur une liste de types (`forTypes`) + +Le corps du test est instancié et enregistré comme autant de tests distincts (nommés automatiquement avec le suffixe `[type 0]`, `[type 1]`, etc.) pour chacun des types spécifiés en argument template : + +```cpp +suite.test("Vector operations").forTypes() = [] { + auto vector = std::vector{}; + vector.push_back(Type{}); +}; +``` + +Rapport généré dans la console lors de l'exécution : + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Container/Vector/Vector operations [type 0]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Container/Vector/Vector operations [type 1]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Container/Vector/Vector operations [type 2]:[/blue] +[green]Passed ✓[/green] +``` + +#### Itération sur une liste de valeurs (`forValues`) + +Le test génère une entrée distincte pour chaque valeur transmise. Si le type est formatable avec `std::format`, la valeur elle-même apparaît entre crochets dans l'intitulé du test (`[10]`, `[50]`, etc.) : + +```cpp +suite.test("Positive scale factor").forValues(10, 50, 100, 500) = [](int scale) { + expect(scale) > 0; +}; +``` + +Rapport généré dans la console lors de l'exécution : + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Scale/Positive scale factor [10]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Scale/Positive scale factor [50]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Scale/Positive scale factor [100]:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Scale/Positive scale factor [500]:[/blue] +[green]Passed ✓[/green] +``` + +#### Itération sur un conteneur ou une plage (`std::ranges`) + +Vous pouvez également transmettre un conteneur standard ou n'importe quelle vue `std::ranges` (comme `std::views::iota` pour tester une séquence numérique paresseuse sans aucune allocation dynamique) ; chaque élément génère alors un cas de test individuel : + +```cpp +// Séquence paresseuse avec std::views::iota (sans allocation mémoire) : +suite.test("Index validation").forValues(std::views::iota(1, 10)) = [](int index) { + expect(index) >= 1; + expect(index) < 10; +}; + +// Conteneur séquentiel standard : +suite.test("Buffer allocation").forValues(std::vector{64uz, 256uz, 1024uz}) = [](std::size_t size) { + auto buffer = std::vector(size, 0); + expect(std::ranges::size(buffer)) == size; +}; + +// Conteneur associatif (itère sur chaque paire std::pair clé-valeur) : +const auto rolePermissions = std::map{ + {"guest", 1}, + {"user", 2}, + {"admin", 4} +}; +suite.test("Role permissions").forValues(rolePermissions) = [](const auto& entry) { + const auto& [role, permissionLevel] = entry; + expect(permissionLevel) > 0; +}; +``` + +### Paramétrage au niveau d'une suite complète + +Le modificateur s'applique aussi à l'enregistrement d'une suite entière (`testSuite(...).forTypes<...>()` ou `testSuite(...).forValues(...)`). Le moteur génère automatiquement une suite de tests distincte pour chaque type (nommée par exemple `"Container/Vector [type 0]"`, `"Container/Vector [type 1]"`) ou pour chaque valeur (nommée `"Math/Prime [2]"`, `"Math/Prime [3]"`) : + +```cpp +auto _ = testSuite("Container/Vector").forTypes() = [](TestSuite& suite) { + suite.test("Default initialization") = [] { + auto container = std::vector{}; + expect(std::ranges::empty(container)); + }; +}; + +auto _ = testSuite("Math/Prime").forValues(2, 3, 5, 7) = [](TestSuite& suite, int prime) { + suite.test("Is prime") = [prime] { + expect(isPrime(prime)); + }; +}; +``` + +--- + +## Surveillance d'événements (`CppUtils::UnitTest::EventWatcher`) + +Pour tester des architectures événementielles ([`CppUtils::Execution::EventQueue`](/fr/reference/execution/eventqueue/), [`CppUtils::Execution::EventDispatcher`](/fr/reference/execution/eventdispatcher/)), `CppUtils::UnitTest::EventWatcher` s'abonne à un événement à sa construction et propose des outils pour enregistrer l'historique des émissions et bloquer jusqu'à réception du nombre attendu d'occurrences sans boucle manuelle : + +```cpp +suite.test("Event dispatching") = [] { + using namespace std::chrono_literals; + + auto queue = CppUtils::Execution::EventQueue{}; + + // Création d'un observateur d'événements typé sur "Notification" + auto watcher = watch<"Notification", int>(queue); + + expect(not watcher.hasOccurred()); + expect(watcher.count()) == 0uz; + + queue.emit<"Notification">(42); + queue.emit<"Notification">(84); + + // Attente active jusqu'à réception de 2 occurrences dans la limite de 500ms + expect(watcher.waitForOccurrences(2uz, 500ms)); + expect(watcher.count()) == 2uz; + auto occurrences = watcher.occurrences(); + expect(occurrences->count) == 2uz; + expect(std::ranges::size(occurrences->payloads)) == 2uz; + expect(occurrences->payloads[0]) == 42; + expect(occurrences->payloads.back()) == 84; +}; +``` + +--- + +## Suivi du cycle de vie des objets : `CppUtils::UnitTest::LifetimeChecker` + +Pour tester la conformité sémantique de vos objets (conteneurs, wrappers RAII, types à sémantique de déplacement, optimisations sans copie), CppUtils fournit [`CppUtils::UnitTest::LifetimeChecker`](/fr/reference/unittest/lifetimechecker/), un objet inerte conçu pour suivre avec précision le cycle de vie de vos types (constructions, destructions, copies et déplacements) et notifier l'appelant. + +### Vérification des compteurs de copie et de déplacement + +L'objet conserve un compteur interne du nombre de copies et de déplacements subis : + +```cpp +import CppUtils; + +suite.test("Move semantics in vector") = [&] { + // Objet inerte avec callback vide + auto checker = CppUtils::UnitTest::LifetimeChecker{[](std::string_view) {}, "Widget"}; + + auto container = std::vector{}; + container.reserve(1); + + // Déplacement dans le conteneur + container.push_back(std::move(checker)); + + expect(container.front().getCopyCount()) == 0uz; + expect(container.front().getMoveCount()) == 1uz; +}; +``` + + + +### Traçabilité des événements de cycle de vie + +Le constructeur accepte une fonction de rappel (*callback*) et un identifiant textuel. À chaque opération sur l'objet (construction, copie, déplacement, destruction), ce rappel est automatiquement invoqué avec le libellé de l'événement correspondant : + +```cpp +suite.test("Lifecycle notifications") = [&] { + auto checker = CppUtils::UnitTest::LifetimeChecker{[](std::string_view event) { + std::println("{}", event); + }, "Resource"}; + + auto copy = checker; + auto moved = std::move(copy); +}; +``` + +La sortie console retrace fidèlement la succession d'appels et l'invalidation de l'objet déplacé : + +```text +Resource() +Resource(const Resource&) +Resource(Resource&&) +~[invalid object]() +~Resource() +~Resource() +``` + +--- + +## Configuration et exécution des suites de tests + +Le lancement de l'ensemble des suites s'effectue dans le point d'entrée principal (`main.cpp`) de votre binaire de tests via `executeTests()` : + +```cpp +import std; +import CppUtils; + +auto main(int argc, char** argv) -> int +{ + // Analyse les arguments en ligne de commande (mode, filter, verbose, failFast, chrono) + auto settings = CppUtils::UnitTest::executeCommands(argc, argv); + if (settings.abort) + return CppUtils::exitSuccess; + + return CppUtils::UnitTest::executeTests(settings); +} +``` + +### Options de configuration (`CppUtils::UnitTest::TestSettings`) + +| Option | Type | Valeur par défaut | Description | +| :--- | :--- | :--- | :--- | +| `verbose` | `bool` | `true` | Affiche l'intitulé et le statut de chaque test unitaire en temps réel. Si `false`, n'affiche que le bilan final. | +| `detail` | `bool` | `true` | Affiche les détails d'exécution (séparateurs, noms complets de suites) lors du déroulement. | +| `chrono` | `bool` | `false` | Mesure et affiche la durée précise de chaque test via `CppUtils::Log::ChronoLogger`. | +| `failFast` | `bool` | `false` | Interrompt immédiatement l'exécution des tests dès le premier échec rencontré. | +| `filter` | `std::string` | `""` | Filtre les suites de tests à exécuter par sous-chaîne tout en résolvant automatiquement leurs prérequis. | +| `report` | `std::string` | `""` | Exporte les résultats d'exécution dans un fichier de rapport (format `junit:chemin.xml` ou `chemin.xml`). | + +#### Syntaxe en ligne de commande + +La fonction [`executeCommands()`](/fr/reference/unittest/functions/#executeCommands) parse les arguments transmis au binaire (`argc`, `argv`) selon les conventions standards POSIX et GNU : + +```bash +# Options standards longues +./UnitTests --filter=Network --chrono --report=junit:report.xml + +# Formes courtes équivalentes +./UnitTests -f=Network -c -r=report.xml + +# Arrêt immédiat dès la première défaillance (fail-fast) +./UnitTests --fail-fast +``` + +| Option longue | Forme courte | Valeur par défaut | Description | +| :--- | :--- | :--- | :--- | +| `--help` | `-h` | - | Affiche l'aide des paramètres disponibles et interrompt le programme. | +| `--mode=` | `-m=` | `debug` | Profil d'exécution prédéfini : `debug` (défaut), `test` (avec chronométrage), `prod` (silencieux et arrêt au premier échec). | +| `--verbose[=]` | `-v[=]` | `true` | Active ou désactive les journaux standards d'exécution. | +| `--detail[=]` | `-d[=]` | `true` | Active ou désactive l'affichage détaillé de l'avancement (noms des tests, durées, métriques de benchmarks, séparateurs). | +| `--fail-fast` | | `false` | Interrompt l'exécution dès la première erreur. | +| `--chrono[=]` | `-c[=]` | `false` | Affiche le temps d'exécution de chaque test. | +| `--filter=` | `-f=` | `""` | Ne lance que les suites de tests dont le nom contient cette sous-chaîne (et résout automatiquement leurs dépendances). | +| `--report=` | `-r=` | `""` | Exporte le rapport d'exécution au format JUnit XML (ex. `-r=report.xml` ou explicitement `--report=junit:report.xml`). | + +### Export de rapports JUnit XML et intégration CI/CD + +L'option `--report=junit:` (ou simplement `-r=`) génère un rapport au standard universel **JUnit XML**. Ce format permet aux plates-formes d'intégration continue d'analyser automatiquement les résultats, d'afficher les métriques temporelles et d'insérer des annotations directement sur les lignes de code défaillantes : + +- **GitLab CI** : Prend en charge le format JUnit XML **de manière native** (aucun plugin ni composant tiers requis) via [`artifacts:reports:junit:`](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsjunit) dans `.gitlab-ci.yml`. +- **GitHub Actions** : Ne parse pas nativement les fichiers XML de tests par défaut. L'affichage d'un résumé graphique et des annotations inline dans les Pull Requests s'effectue via une action GitHub dédiée du marketplace (telle que [`EnricoMi/publish-unit-test-result-action`](https://github.com/EnricoMi/publish-unit-test-result-action) ou [`dorny/test-reporter`](https://github.com/dorny/test-reporter)). +- **Jenkins** : Exploite directement le rapport via son plugin officiel standard [JUnit Plugin](https://plugins.jenkins.io/junit/). + + + +```yaml +# .gitlab-ci.yml : Aucun plugin requis, support natif de GitLab CI +test: + stage: test + script: + - xmake run CppUtils-UnitTests --report=junit:build/test-results.xml + artifacts: + when: always + reports: + junit: build/test-results.xml +``` + + +```yaml +# .github/workflows/test.yml : Publication via l'action du marketplace + - name: Run unit tests + run: | + xmake run CppUtils-UnitTests --report=junit:build/test-results.xml + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: "build/test-results.xml" +``` + + + +### Isolation par processus et gestion du temps maximal + +Chaque test unitaire est exécuté dans un processus enfant dédié. Les violations d'accès mémoire, les arrêts brutaux et les blocages infinis sont ainsi interceptés de manière étanche sans jamais interrompre l'exécuteur de tests ni affecter les suites suivantes. + +Cette architecture offre deux garanties majeures : +- **Résilience face aux plantages critiques** : Si un test déclenche un signal fatal (`SIGSEGV`, `SIGABRT`, etc.), le signal est capturé par l'exécuteur parent. Le test est consigné en échec avec le détail du signal reçu, tandis que le reste des tests continue son exécution normale. +- **Interruption stricte en cas de timeout** : Si un test dépasse son temps limite alloué (10 secondes par défaut, ou personnalisé via `.timeout(2s)`), il est interrompu immédiatement, évitant les blocages indéfinis de la suite de tests. + +--- + +## Rapport et diagnostics d'exécution + +### Exécution réussie + +Lors d'une exécution sans erreur, le rapport affiche le déroulement structuré des tests : + +```terminal +2 tests found. Execution: +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Math/Calculator/Addition:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Socket/TCP/Accept connection:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +Test results +All tests passed successfully +- 2 successful tests +``` + +### Défaillance ciblée et propagation aux dépendances + +En cas d'échec d'un composant de base, le rapport isole distinctement la ligne fautive avec les valeurs évaluées, et marque automatiquement les suites dépendantes comme sautées : + +```terminal +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Socket/Init:[/blue] +[green]Passed ✓[/green] +[blue]──────────────────────────────────────────────────────────────[/blue] +[blue]Network/Socket/TCP:[/blue] + [red]🛇 ERROR 🛇[/red] +The following test didn't pass: +Network/Socket/TCP + In tests/Network/Socket.mpp +At line 48, column 3 +In expect(lhs) == rhs +with lhs: +500 +and rhs: +200 + +[red]Failed ✕[/red] +[blue]──────────────────────────────────────────────────────────────[/blue] +[yellow]Skipping test suite Network/Client due to failed prerequisites in test suite Network/Socket ⚠[/yellow] +[blue]──────────────────────────────────────────────────────────────[/blue] +Test results +The tests failed: +- 1 successful tests +- 1 failed tests + - Network/Socket +- 1 skipped tests + - Network/Client (due to failed prerequisites) +``` diff --git a/docs/src/content/docs/fr/index.mdx b/docs/src/content/docs/fr/index.mdx index fac951dd..13fb1bc7 100644 --- a/docs/src/content/docs/fr/index.mdx +++ b/docs/src/content/docs/fr/index.mdx @@ -16,21 +16,28 @@ hero: variant: minimal --- -import { Card, CardGrid } from '@astrojs/starlight/components'; +import { Card, CardGrid, LinkButton } from '@astrojs/starlight/components'; import { ContributorList } from 'astro-contributors'; export const base = import.meta.env.BASE_URL.replace(/\/$/, ''); ## Découvrir la documentation +CppUtils se compose de plusieurs grandes bibliothèques modulaires (tests unitaires, réseau, topologie de graphes) accompagnées de nombreux utilitaires spécialisés. Les modules C++20 permettent d'importer aisément l'ensemble en une seule directive (`import CppUtils;`) sans surcharger le binaire final, seuls les composants effectivement utilisés étant intégrés lors de la compilation. + - Installation pas-à-pas avec XMake et Clang, puis compilation de votre premier programme modulaire C++26. + Guide pas à pas pour configurer votre environnement et intégrer CppUtils à vos projets. + + + + + Conçu pour le C++ moderne sans macros : assertions asynchrones, fuzzing, benchmarks et résilience aux crashs. - + Guide pratique pour utiliser `UniqueLocker` et `SharedLocker` afin de sécuriser l'accès aux données partagées. @@ -41,6 +48,46 @@ export const base = import.meta.env.BASE_URL.replace(/\/$/, ''); +## Philosophie de la bibliothèque + +CppUtils repose sur des principes directeurs sans concession : + + + Réunit l'essentiel (conteneurs, concurrence, réseau, parsing, tests, crypto, etc.) pour éliminer la prolifération de dépendances externes. Grâce aux **modules C++**, cette richesse favorise une réutilisabilité maximale sans aucun surcoût : + - **À la carte ou globale** : importez un sous-domaine ciblé (`import CppUtils.Container;`) ou tout le catalogue (`import CppUtils;`). + - **Zéro empreinte superflue** : seuls les composants effectivement appelés sont incorporés au binaire final. + - **Isolation hermétique & compilation accélérée** : chaque module n'est compilé qu'une seule fois, allégeant massivement les temps de build face aux en-têtes préprocesseur (`#include`). + + + + Aucun carcan ni contamination architecturale : CppUtils ne cherche jamais à imposer ses types ni à coloniser votre codebase. + - **Conception non invasive & zéro contagion** : contrairement à des frameworks invasifs comme **Qt** qui imposent partout leur méta-compilateur propriétaire (`moc`), leurs types fermés (`QString`, `QByteArray`) et de lourdes hiérarchies d'héritage, CppUtils reste strictement non invasif et ne propage aucune adhérence virale : l'objectif n'est jamais de vous rendre dépendant pour vous retenir dans un écosystème, refusant toute forme de captivité technique (*vendor lock-in*). + - **Programmation par contrat** : les interfaces s'expriment via des concepts standards (`std::ranges::input_range`, `std::formattable`, `std::invocable`, etc.) plutôt que des types figés et imposés : vos conteneurs se propagent sans copie ni conversion superflue (en savoir plus sur la [programmation par contrat avec `auto` et les concepts sur mon blog](https://morgancaron.github.io/articles/cpp/auto#abbreviated-function-template-depuis-c20)). + - **Adoption isolée** : intégrez un composant unique (`MultiKeyMap`, `UniqueLocker`) en toute indépendance, sans devoir importer le reste du catalogue. + + + + Alignement assumé sur le dernier standard (**C++26**) pour délivrer immédiatement l'état de l'art : modules natifs, typage monadique, concepts avancés, sécurité accrue et réflexivité. + - **Zéro compromis legacy** : aucun maintien artificiel de pratiques obsolètes ni accumulation de dette technique. + - **Transition pragmatique** : des gardes de fonctionnalités ciblées (`__cpp_lib_*`) facilitent la compilation sur les compilateurs récents au support C++26 encore partiel, sans alourdir le code de compatibilité rétrograde. + - **Architecture pérenne** : refactorisations continues pour préserver une base de code moderne, saine et prête pour l'avenir. + + + + La performance maximale sans jamais sacrifier la clarté ni la sécurité : + - **Lisibilité & simplicité** : interfaces épurées, transparentes et auto-documentées, sans complexité accidentelle ni magie obscure. + - **Performance sans compromis** : abstractions à coût nul (*zero-cost*), agencement mémoire adapté au cache et évaluation maximale à la compilation (`constexpr`, `consteval`). + - **Bonnes pratiques modernes** : gestion par RAII systématique, typage fort validé par concepts et rejet des macros préprocesseur (plusieurs de ces [bonnes pratiques sont détaillées sur mon blog](https://morgancaron.github.io/articles/cpp/)). + + +## Soutenir le projet + +Si vous appréciez CppUtils et souhaitez soutenir son développement indépendant, vous pouvez parrainer le projet sur GitHub Sponsors : + + + Sponsoriser sur GitHub + + ## Contributeurs diff --git a/docs/src/plugins/remark-cppreference.mjs b/docs/src/plugins/remark-cppreference.mjs index 7ec9325f..b058eaa9 100644 --- a/docs/src/plugins/remark-cppreference.mjs +++ b/docs/src/plugins/remark-cppreference.mjs @@ -12,9 +12,9 @@ function getCppReferenceUrl(rawSymbol) { const clean = rawSymbol.trim(); if (Object.hasOwn(symbols, clean)) return symbols[clean]; - const match = clean.match(/\b(std::[a-zA-Z0-9_:]+)/); + const match = clean.match(/^std::[a-zA-Z0-9_:]+/); if (!match) return null; - const sym = match[1]; + const sym = match[0]; if (Object.hasOwn(symbols, sym)) return symbols[sym]; const parts = sym.split('::'); diff --git a/docs/src/plugins/remark-cpputils.mjs b/docs/src/plugins/remark-cpputils.mjs new file mode 100644 index 00000000..f2a5cd15 --- /dev/null +++ b/docs/src/plugins/remark-cpputils.mjs @@ -0,0 +1,59 @@ +import { visit } from 'unist-util-visit'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const symbolsPath = path.resolve(__dirname, '../data/cpputils-symbols.json'); + +function loadSymbols() { + if (!fs.existsSync(symbolsPath)) return null; + try { + return JSON.parse(fs.readFileSync(symbolsPath, 'utf8')); + } catch { + return null; + } +} + +function getCppUtilsUrl(rawSymbol, symbols, lang) { + if (!rawSymbol || !symbols) return null; + const clean = rawSymbol.trim(); + if (!clean.startsWith('CppUtils::')) return null; + + if (Object.hasOwn(symbols, clean)) return `/${lang}${symbols[clean]}`; + + const withoutTemplate = clean.replace(/<.*>$/, ''); + if (withoutTemplate !== clean && Object.hasOwn(symbols, withoutTemplate)) { + return `/${lang}${symbols[withoutTemplate]}`; + } + + return null; +} + +export function remarkCppUtils() { + return (tree, file) => { + const symbols = loadSymbols(); + if (!symbols || Object.keys(symbols).length === 0) return; + + const filePath = file?.history?.[0] || file?.path || ''; + const isEn = filePath.includes('/en/') || filePath.endsWith('/en.mdx'); + const lang = isEn ? 'en' : 'fr'; + + visit(tree, 'inlineCode', (node, index, parent) => { + if (!parent || parent.type === 'link') return; + const url = getCppUtilsUrl(node.value, symbols, lang); + if (!url) return; + + parent.children[index] = { + type: 'link', + url, + children: [ + { + type: 'inlineCode', + value: node.value, + }, + ], + }; + }); + }; +} diff --git a/docs/src/plugins/remark-terminal.mjs b/docs/src/plugins/remark-terminal.mjs new file mode 100644 index 00000000..c092d1ef --- /dev/null +++ b/docs/src/plugins/remark-terminal.mjs @@ -0,0 +1,163 @@ +import { visit } from 'unist-util-visit'; + +function parseTextWithTagsAndAnsi(text) { + const nodes = []; + const tokenRegex = /(\[(?:\/)?(?:blue|red|green|yellow|cyan|magenta|dim|bold)\]|\x1b\[[0-9;]*m)/g; + let lastIndex = 0; + let match; + const activeClasses = []; + + function addTextSegment(segment) { + if (!segment) return; + nodes.push({ + type: 'element', + tagName: 'span', + properties: activeClasses.length > 0 ? { className: [...new Set(activeClasses)] } : {}, + children: [{ type: 'text', value: segment }], + }); + } + + while ((match = tokenRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + addTextSegment(text.slice(lastIndex, match.index)); + } + + const token = match[1]; + if (token.startsWith('\x1b[')) { + const ansiCodes = token.slice(2, -1).split(';'); + for (const ansiCode of ansiCodes) { + if (ansiCode === '0' || ansiCode === '') { + activeClasses.length = 0; + } else if (ansiCode === '39') { + for (let i = activeClasses.length - 1; i >= 0; i--) { + if (['term-red', 'term-green', 'term-blue', 'term-yellow', 'term-magenta', 'term-cyan'].includes(activeClasses[i])) { + activeClasses.splice(i, 1); + } + } + } else if (ansiCode === '31') { + activeClasses.push('term-red'); + } else if (ansiCode === '32') { + activeClasses.push('term-green'); + } else if (ansiCode === '33') { + activeClasses.push('term-yellow'); + } else if (ansiCode === '34') { + activeClasses.push('term-blue'); + } else if (ansiCode === '35') { + activeClasses.push('term-magenta'); + } else if (ansiCode === '36') { + activeClasses.push('term-cyan'); + } else if (ansiCode === '90' || ansiCode === '2') { + activeClasses.push('term-dim'); + } else if (ansiCode === '1') { + activeClasses.push('term-bold'); + } else if (ansiCode === '22') { + for (let i = activeClasses.length - 1; i >= 0; i--) { + if (activeClasses[i] === 'term-bold' || activeClasses[i] === 'term-dim') { + activeClasses.splice(i, 1); + } + } + } + } + } else if (token.startsWith('[/')) { + const tagColor = token.slice(2, -1); + const className = 'term-' + tagColor; + const lastIdx = activeClasses.lastIndexOf(className); + if (lastIdx !== -1) { + activeClasses.splice(lastIdx, 1); + } + } else if (token.startsWith('[')) { + const tagColor = token.slice(1, -1); + activeClasses.push('term-' + tagColor); + } + + lastIndex = tokenRegex.lastIndex; + } + + if (lastIndex < text.length) { + addTextSegment(text.slice(lastIndex)); + } + + return nodes; +} + +export function remarkTerminal() { + return (tree) => { + visit(tree, 'code', (node) => { + if (node.lang !== 'terminal' && node.lang !== 'console') { + return; + } + + const lines = node.value.split('\n'); + const codeChildren = []; + + for (let lineIndex = 0; lineIndex < lines.length; ++lineIndex) { + const currentLine = lines[lineIndex]; + const parsedLineNodes = parseTextWithTagsAndAnsi(currentLine); + codeChildren.push(...parsedLineNodes); + if (lineIndex + 1 < lines.length) { + codeChildren.push({ type: 'text', value: '\n' }); + } + } + + const titleMatch = node.meta?.match(/title=["']([^"']+)["']/); + const title = titleMatch ? titleMatch[1] : (node.lang === 'terminal' ? 'Terminal' : 'Console'); + + delete node.lang; + delete node.meta; + delete node.value; + node.type = 'paragraph'; + node.children = []; + node.data = { + hName: 'div', + hProperties: { className: ['terminal-container', 'not-content'] }, + hChildren: [ + { + type: 'element', + tagName: 'div', + properties: { className: ['terminal-header'] }, + children: [ + { + type: 'element', + tagName: 'span', + properties: { className: ['terminal-dot', 'terminal-dot-red'] }, + children: [], + }, + { + type: 'element', + tagName: 'span', + properties: { className: ['terminal-dot', 'terminal-dot-yellow'] }, + children: [], + }, + { + type: 'element', + tagName: 'span', + properties: { className: ['terminal-dot', 'terminal-dot-green'] }, + children: [], + }, + { + type: 'element', + tagName: 'span', + properties: { className: ['terminal-title'] }, + children: [{ type: 'text', value: title }], + }, + ], + }, + { + type: 'element', + tagName: 'pre', + properties: { className: ['terminal-content'] }, + children: [ + { + type: 'element', + tagName: 'code', + properties: {}, + children: codeChildren, + }, + ], + }, + ], + }; + }); + }; +} + diff --git a/docs/src/styles/components.css b/docs/src/styles/components.css index b262b779..4b6b2374 100644 --- a/docs/src/styles/components.css +++ b/docs/src/styles/components.css @@ -237,3 +237,248 @@ site-search dialog::backdrop { border-radius: 2px; padding: 0.05rem 0.2rem; } + +:root .scroll-to-top-button { + width: 44px; + height: 44px; + border-radius: 50%; + background: radial-gradient(circle at 50% 10%, rgb(56 189 248 / 22%), transparent 70%), rgb(15 23 42 / 85%); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgb(56 189 248 / 50%); + box-shadow: 0 6px 20px -2px rgb(0 0 0 / 50%), 0 0 18px -2px rgb(56 189 248 / 28%), inset 0 1px 0 0 rgb(255 255 255 / 22%), inset 0 0 12px rgb(56 189 248 / 18%); + color: var(--sl-color-accent); + transition: opacity 0.25s ease, visibility 0.25s ease, transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1), + border-color 0.2s ease, box-shadow 0.25s ease, background 0.2s ease, color 0.2s ease; + bottom: 2rem; + right: 1.5rem; + z-index: 90; + + svg { + width: 22px; + height: 22px; + stroke: var(--sl-color-accent); + stroke-width: 2.2; + transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1), stroke 0.2s ease, filter 0.2s ease; + } + + &.visible { + opacity: 1; + visibility: visible; + } + + &:hover { + transform: translateY(-4px); + background: radial-gradient(circle at 50% 10%, rgb(56 189 248 / 35%), transparent 70%), rgb(24 38 68 / 92%); + border-color: var(--sl-color-accent-high, #7ad1ff); + color: var(--sl-color-accent-high, #7ad1ff); + box-shadow: 0 12px 30px -4px rgb(0 0 0 / 60%), 0 0 28px 2px rgb(56 189 248 / 45%), + inset 0 1px 0 0 rgb(255 255 255 / 35%), inset 0 0 16px rgb(56 189 248 / 25%); + + svg { + stroke: var(--sl-color-accent-high, #7ad1ff); + transform: translateY(-3px); + filter: drop-shadow(0 0 6px var(--sl-color-accent)); + } + } + + &:active { + transform: translateY(-1px); + background: rgb(15 23 42 / 98%); + border-color: var(--sl-color-accent); + box-shadow: 0 3px 10px rgb(0 0 0 / 40%), 0 0 12px rgb(56 189 248 / 30%), inset 0 1px 0 0 rgb(255 255 255 / 15%); + + svg { + transform: translateY(0); + filter: none; + } + } + + &.keyboard-focus { + outline: 2px solid var(--sl-color-accent); + outline-offset: 3px; + } +} + +@media (width >= 72rem) { + html[data-has-toc] body:not(.right-side-bar-collapsed) .scroll-to-top-button { + right: calc(var(--sl-sidebar-width) + 1.5rem); + } + + html[data-has-toc] body.right-side-bar-collapsed .scroll-to-top-button { + right: calc(50px + 1.5rem); + } +} + +:root[data-theme='light'] .scroll-to-top-button { + background: radial-gradient(circle at 50% 10%, rgb(2 132 199 / 10%), transparent 70%), #ffffff; + border: 1px solid rgb(2 132 199 / 40%); + box-shadow: 0 4px 18px -2px rgb(15 23 42 / 10%), 0 0 16px -2px rgb(2 132 199 / 20%), inset 0 1px 0 0 #ffffff, inset 0 0 10px rgb(2 132 199 / 10%); + color: var(--sl-color-accent); + + svg { + stroke: var(--sl-color-accent); + stroke-width: 2.2; + } + + &:hover { + transform: translateY(-4px); + background: radial-gradient(circle at 50% 10%, rgb(2 132 199 / 18%), transparent 70%), #f0f9ff; + border-color: var(--sl-color-accent); + color: var(--sl-color-accent-high, #0369a1); + box-shadow: 0 12px 28px -4px rgb(15 23 42 / 16%), 0 0 24px 2px rgb(2 132 199 / 35%), + inset 0 1px 0 0 #ffffff, inset 0 0 14px rgb(2 132 199 / 16%); + + svg { + stroke: var(--sl-color-accent-high, #0369a1); + transform: translateY(-3px); + filter: drop-shadow(0 0 5px rgb(2 132 199 / 35%)); + } + } + + &:active { + transform: translateY(-1px); + background: #e0f2fe; + box-shadow: 0 3px 10px rgb(15 23 42 / 12%), 0 0 10px rgb(2 132 199 / 20%); + + svg { + transform: translateY(0); + filter: none; + } + } +} + +:root .scroll-to-top-btn-tooltip { + background: rgb(15 23 42 / 95%); + border: 1px solid rgb(255 255 255 / 14%); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + color: #f1f5f9; + border-radius: 6px; + padding: 0.35rem 0.65rem; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.02em; + box-shadow: 0 4px 16px rgb(0 0 0 / 40%); + top: -42px; + + /* Tooltip arrow */ + > div { + border-top-color: rgb(15 23 42 / 95%); + } +} + +:root[data-theme='light'] .scroll-to-top-btn-tooltip { + background: #ffffff; + border: 1px solid #cbd5e1; + color: #0f172a; + box-shadow: 0 4px 16px rgb(15 23 42 / 12%); + + > div { + border-top-color: #ffffff; + } +} + +.terminal-container { + margin: 1.5rem 0; + border-radius: 8px; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%); + background-color: #121316; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35); + + .terminal-header { + display: flex; + align-items: center; + gap: 0.45rem; + padding: 0.5rem 0.85rem; + background-color: var(--sl-color-black); + border-bottom: 1px solid color-mix(in srgb, var(--sl-color-gray-5), transparent 25%); + user-select: none; + + .terminal-dot { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; + + &.terminal-dot-red { + background-color: #ef4444; + } + + &.terminal-dot-yellow { + background-color: #eab308; + } + + &.terminal-dot-green { + background-color: #22c55e; + } + } + + .terminal-title { + margin-left: 0.5rem; + font-size: 0.8rem; + color: var(--sl-color-gray-3); + font-family: var(--sl-font, system-ui); + } + } + + .terminal-content { + margin: 0; + padding: 0.85rem 1rem; + background-color: #121316; + color: #e2e8f0; + font-family: var(--sl-font-mono, monospace); + font-size: var(--sl-text-code, 0.875rem); + line-height: var(--sl-line-height, 1.5); + overflow-x: auto; + white-space: pre; + + code { + font-family: inherit; + font-size: inherit; + background: transparent; + padding: 0; + color: inherit; + } + + .term-blue { + color: #60a5fa; + } + + .term-red { + color: #f87171; + } + + .term-green { + color: #4ade80; + } + + .term-yellow { + color: #facc15; + } + + .term-cyan { + color: #38bdf8; + } + + .term-magenta { + color: #c084fc; + } + + .term-dim { + color: #64748b; + } + + .term-bold { + font-weight: 700; + } + } +} + +.sl-markdown-content table { + td code, + th code { + white-space: nowrap; + } +} diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css index 74783130..6ab7d3a9 100644 --- a/docs/src/styles/custom.css +++ b/docs/src/styles/custom.css @@ -3,3 +3,13 @@ @import url('./components.css'); @import url('./reference.css'); @import url('./theme.css'); + +html { + scroll-behavior: smooth; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} diff --git a/docs/src/styles/layout.css b/docs/src/styles/layout.css index 0485137f..31dd6f5b 100644 --- a/docs/src/styles/layout.css +++ b/docs/src/styles/layout.css @@ -99,20 +99,33 @@ header.header { width: var(--sl-sidebar-width); height: calc(100vh - var(--sl-nav-height)); padding-top: 0; - padding-bottom: 1rem; + padding-bottom: 0; background-color: transparent; background-image: linear-gradient(180deg, rgb(8 12 20 / 96%) 0%, rgb(8 12 20 / 82%) 30%, rgb(8 12 20 / 25%) 65%, transparent 88%); border-inline-start: 1px solid var(--sl-color-hairline-shade); backdrop-filter: blur(8px); transition: width 0.3s cubic-bezier(0.16, 1, 0.3, 1), padding 0.3s cubic-bezier(0.16, 1, 0.3, 1); - overflow: hidden auto; - scrollbar-width: thin; - scrollbar-color: var(--sl-color-hairline-shade) transparent; + overflow: hidden; + display: flex; + flex-direction: column; .right-sidebar-panel { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-height: 0; + height: 100%; width: var(--sl-sidebar-width); padding-top: 0.75rem; - overflow-x: hidden; + overflow: hidden; + + > .sl-container { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-height: 0; + height: 100%; + } nav { transition: opacity 0.2s ease-in-out; @@ -138,6 +151,7 @@ header.header { .toggle-left-side-bar-btn, .toggle-right-side-bar-btn { + flex-shrink: 0; width: 2.25rem; height: 2.25rem; border: none; @@ -294,16 +308,48 @@ body.right-side-bar-collapsed { } starlight-toc { - display: block; + display: flex; + flex-direction: column; + flex: 1 1 0; + min-height: 0; + overflow: hidden; } starlight-toc nav { display: flex; flex-direction: column; + flex: 1 1 0; + min-height: 0; + overflow: hidden; +} + +starlight-toc nav > ul { + flex: 1 1 0; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding-right: 0.25rem; + padding-bottom: 1.5rem; + scrollbar-width: thin; + scrollbar-color: var(--sl-color-hairline-shade) transparent; +} + +starlight-toc nav > ul::-webkit-scrollbar { + width: 4px; +} + +starlight-toc nav > ul::-webkit-scrollbar-track { + background: transparent; +} + +starlight-toc nav > ul::-webkit-scrollbar-thumb { + background: var(--sl-color-hairline-shade); + border-radius: 4px; } starlight-toc h2 { display: block; + flex-shrink: 0; font-size: 0.72rem; font-weight: 700; line-height: 1; @@ -321,6 +367,7 @@ starlight-toc h2 { starlight-toc .toc-progress { display: block; + flex-shrink: 0; height: 0.1875rem; margin: 0 0.5rem 0.85rem; overflow: hidden; diff --git a/docs/src/styles/reference.css b/docs/src/styles/reference.css index 7bf86575..6cb92db7 100644 --- a/docs/src/styles/reference.css +++ b/docs/src/styles/reference.css @@ -6,34 +6,6 @@ main:has([data-page-type="reference"]) .sl-markdown-content :is(h2, .sl-heading- position: relative; margin-top: 1.5rem; margin-bottom: 0.5rem; - overflow-x: auto; - - .member-anchor { - position: absolute; - top: 0.35rem; - right: 0.5rem; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--sl-color-gray-4); - text-decoration: none; - opacity: 0; - transition: opacity 0.15s ease, color 0.15s ease; - z-index: 2; - padding: 0.2rem; - border-radius: 4px; - user-select: none; - - &:hover { - color: var(--sl-color-accent-high); - background: rgb(255 255 255 / 8%); - } - } - - &:hover .member-anchor, - .member-anchor:focus { - opacity: 1; - } .expressive-code { --ec-brdCol: transparent; @@ -41,6 +13,7 @@ main:has([data-page-type="reference"]) .sl-markdown-content :is(h2, .sl-heading- --code-background: transparent; .frame { + position: static; background: transparent; border: none; box-shadow: none; @@ -49,6 +22,7 @@ main:has([data-page-type="reference"]) .sl-markdown-content :is(h2, .sl-heading- pre { background: transparent; padding: 0; + padding-right: 5.5rem; border: none; white-space: pre-wrap; word-break: break-word; @@ -61,20 +35,166 @@ main:has([data-page-type="reference"]) .sl-markdown-content :is(h2, .sl-heading- } .copy { - display: none; + display: flex; + position: absolute; + top: 0.55rem; + right: 0.75rem; + z-index: 5; + + button { + opacity: 0.65; + width: 1.85rem; + height: 1.85rem; + border-radius: 0.25rem; + transition: opacity 0.15s ease, background-color 0.15s ease; + + &:hover, + &:focus { + opacity: 1; + } + } + } + } +} + +.member-card { + position: relative; + margin-top: 1.25rem; + margin-bottom: 1.75rem; + padding: 1.25rem 1.5rem; + background: rgb(15 23 42 / 40%); + border: 1px solid var(--sl-color-hairline-shade); + border-radius: 0.75rem; + box-shadow: 0 4px 16px -4px rgb(0 0 0 / 20%); + transition: border-color 0.2s ease, box-shadow 0.2s ease; + + &:hover { + border-color: var(--sl-color-gray-5); + + .member-anchor { + opacity: 0.95; + } + + .expressive-code .copy button { + opacity: 0.95; + } + } + + &:target, + &:has(:target) { + border-color: var(--sl-color-accent); + box-shadow: 0 0 0 1px var(--sl-color-accent), 0 8px 24px -4px rgb(56 189 248 / 20%); + } + + .member-signature { + position: relative; + margin-top: 0; + margin-bottom: 0.85rem; + padding: 0.75rem 1rem; + background: rgb(2 6 23 / 60%); + border: 1px solid var(--sl-color-hairline-shade); + border-radius: 0.5rem; + + .member-anchor { + position: absolute; + top: 0.55rem; + right: 2.95rem; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.85rem; + height: 1.85rem; + border-radius: 0.25rem; + color: var(--sl-color-gray-3); + text-decoration: none; + opacity: 0.65; + transition: opacity 0.15s ease, color 0.15s ease, background-color 0.15s ease; + z-index: 5; + user-select: none; + padding: 0; + border: none; + background: transparent; + line-height: 1; + + &:hover, + &:focus { + opacity: 1; + color: var(--sl-color-accent-high); + background: var(--sl-color-gray-6); + } + } + } + + :is(h3, .sl-heading-wrapper.level-h3):first-child { + margin-top: 0; + margin-bottom: 0.75rem; + font-size: 1.25rem; + } + + table { + margin-top: 0.85rem; + margin-bottom: 0.85rem; + } + + > :last-child { + margin-bottom: 0; + } +} + +:root[data-theme='light'] .member-card { + background: rgb(255 255 255 / 85%); + border: 1px solid rgb(226 232 240 / 90%); + box-shadow: 0 2px 10px -2px rgb(15 23 42 / 4%); + + &:hover { + border-color: rgb(203 213 225); + } + + &:target, + &:has(:target) { + border-color: var(--sl-color-accent-high); + box-shadow: 0 0 0 1px var(--sl-color-accent-high), 0 8px 24px -4px rgb(2 132 199 / 15%); + } + + .member-signature { + background: rgb(248 250 252 / 90%); + border-color: rgb(226 232 240); + + .member-anchor { + color: var(--sl-color-gray-2); + + &:hover, + &:focus { + color: var(--sl-color-accent-high); + background: rgb(226 232 240 / 60%); + } } } } -.sl-markdown-content :is(h3, .sl-heading-wrapper.level-h3) { - font-size: 1.15rem; +.sl-markdown-content :is(h3:not(.reference-card-title), .sl-heading-wrapper.level-h3) { + font-size: 1.625rem; + font-weight: 600; + color: var(--sl-color-white); + margin-top: 1.75rem; +} + +.sl-markdown-content :is(h4, .sl-heading-wrapper.level-h4) { + font-size: 1.25rem; font-weight: 600; - color: var(--sl-color-gray-2); + color: var(--sl-color-white); margin-top: 1.5rem; } +.sl-markdown-content :is(h5, .sl-heading-wrapper.level-h5) { + font-size: 1.05rem; + font-weight: 600; + color: var(--sl-color-white); + margin-top: 1.25rem; +} + main:has(.reference-masonry) .sl-container { - max-width: 96rem; + max-width: 100%; } .reference-container, @@ -88,18 +208,30 @@ main:has(.reference-masonry) .sl-container { column-gap: 1.25rem; } -@container (min-width: 58rem) { +@container (min-width: 44rem) { .reference-masonry { column-count: 2; } } -@container (min-width: 84rem) { +@container (min-width: 68rem) { .reference-masonry { column-count: 3; } } +@container (min-width: 94rem) { + .reference-masonry { + column-count: 4; + } +} + +@container (min-width: 120rem) { + .reference-masonry { + column-count: 5; + } +} + .reference-category-card { break-inside: avoid; page-break-inside: avoid; @@ -107,11 +239,11 @@ main:has(.reference-masonry) .sl-container { display: inline-block; width: 100%; margin-top: 0; - margin-bottom: 1.25rem; + margin-bottom: 1rem; background: rgb(15 23 42 / 50%); border: 1px solid var(--sl-color-hairline-shade); border-radius: 0.75rem; - padding: 1rem 1.15rem; + padding: 0.85rem 1rem; box-shadow: 0 4px 16px -4px rgb(0 0 0 / 25%); backdrop-filter: blur(8px); transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; @@ -140,21 +272,25 @@ main:has(.reference-masonry) .sl-container { border-bottom: 1px solid var(--sl-color-hairline-shade); } -.sl-markdown-content { - .reference-card-title { - margin: 0 !important; - font-size: 0.95rem; - font-weight: 600; - color: var(--sl-color-white); - word-break: break-word; +.sl-markdown-content .reference-category-card h3.reference-card-title { + margin: 0; + margin-top: 0; + margin-bottom: 0; + font-size: 0.8rem; + font-weight: 600; + line-height: 1.3; + color: var(--sl-color-white); + word-break: break-word; +} - code { - background: transparent; - padding: 0; - font-size: inherit; - color: inherit; - } - } +.sl-markdown-content .reference-category-card h3.reference-card-title code { + background: transparent; + padding: 0; + font-size: inherit; + color: inherit; +} + +.sl-markdown-content { .reference-card-list { list-style: none; @@ -231,7 +367,8 @@ main:has(.reference-masonry) .sl-container { border-color: rgb(192 132 252 / 30%); } -.ref-badge-concept { +.ref-badge-concept, +.ref-badge-concepts { background: rgb(245 158 11 / 15%); color: #fbbf24; border-color: rgb(251 191 36 / 30%); diff --git a/include/Stl/expected.hpp b/include/Stl/expected.hpp index 9662350c..db71692e 100644 --- a/include/Stl/expected.hpp +++ b/include/Stl/expected.hpp @@ -20,9 +20,7 @@ namespace Stl constexpr unexpected(unexpected&&) = default; template - requires (!std::is_same_v, unexpected> && - !std::is_same_v, std::in_place_t> && - std::is_constructible_v) + requires (!std::is_same_v, unexpected> && !std::is_same_v, std::in_place_t> && std::is_constructible_v) constexpr explicit unexpected(Err&& e): unex{std::forward(e)} {} @@ -246,8 +244,7 @@ namespace Stl constexpr expected& operator=(const expected&); constexpr expected& operator=(expected&&) noexcept( - std::is_nothrow_move_constructible_v and std::is_nothrow_move_assignable_v && - std::is_nothrow_move_constructible_v and std::is_nothrow_move_assignable_v); + std::is_nothrow_move_constructible_v and std::is_nothrow_move_assignable_v && std::is_nothrow_move_constructible_v and std::is_nothrow_move_assignable_v); template constexpr expected& operator=(U&&); template @@ -279,8 +276,7 @@ namespace Stl } constexpr void swap(expected& other) noexcept( - std::is_nothrow_move_constructible_v and std::is_nothrow_swappable_v && - std::is_nothrow_move_constructible_v and std::is_nothrow_swappable_v) + std::is_nothrow_move_constructible_v and std::is_nothrow_swappable_v && std::is_nothrow_move_constructible_v and std::is_nothrow_swappable_v) { if (has_value() and other.has_value()) { diff --git a/modules/Container/MultiKeyMap.mpp b/modules/Container/MultiKeyMap.mpp index dd49e2f9..f35d0565 100644 --- a/modules/Container/MultiKeyMap.mpp +++ b/modules/Container/MultiKeyMap.mpp @@ -1,7 +1,7 @@ export module CppUtils.Container.MultiKeyMap; import std; -import CppUtils.String.Hash; +export import CppUtils.String.Hash; import CppUtils.Type.Utility; export namespace CppUtils::Container diff --git a/modules/Container/NetworkPtr.mpp b/modules/Container/NetworkPtr.mpp index d3c8a202..c0a888f1 100644 --- a/modules/Container/NetworkPtr.mpp +++ b/modules/Container/NetworkPtr.mpp @@ -129,19 +129,31 @@ export namespace CppUtils::Container inline constexpr auto detachChildren() -> void { - for (auto& child : m_children) - if (auto childPtr = std::visit([](auto&& pointer) -> SharedPtr { + for (auto children = std::move(m_children); auto& child : children) + { + const auto needsDistanceUpdate = std::holds_alternative(child); + if (auto childPointer = std::visit([](auto&& pointer) -> SharedPtr { if constexpr (std::is_same_v, SharedPtr>) return pointer; else return pointer.lock(); }, child)) { - if (not childPtr) + if (not childPointer) continue; - auto accessor = childPtr->uniqueAccess(); - detachChild(accessor.value()); + auto accessor = childPointer->uniqueAccess(); + auto& parents = accessor->m_parents; + if (auto parentIterator = std::ranges::find_if(parents, [this](const auto& parentWeak) -> bool { + return Thread::ownerEqual(parentWeak, m_weakThis); + }); + parentIterator != std::ranges::cend(parents)) + { + parents.erase(parentIterator); + if (needsDistanceUpdate) + accessor->updateDistance(m_weakThis, m_distanceFromRoot); + } } + } } inline constexpr auto detachParent(SharedPtr& parent) -> void @@ -157,15 +169,23 @@ export namespace CppUtils::Container inline constexpr auto detachParents() -> void { - if (auto thisPtr = m_weakThis.lock(); thisPtr) - for (const auto& parentWeak : m_parents) - if (auto parent = parentWeak.lock()) + for (auto parents = std::move(m_parents); const auto& parentWeak : parents) + { + if (auto parent = parentWeak.lock()) + { + auto accessor = parent->uniqueAccess(); + auto& children = accessor->m_children; + if (auto childIterator = std::ranges::find_if(children, [this](const auto& child) -> bool { + return std::visit([this](const auto& childPointer) -> bool { + return Thread::ownerEqual(m_weakThis, childPointer); + }, child); + }); + childIterator != std::ranges::end(children)) { - auto accessor = Thread::MultipleAccessor{*parent, *thisPtr}; - auto& parentRef = std::get<0>(accessor.values); - auto& childRef = std::get<1>(accessor.values); - parentRef.detachChild(childRef); + children.erase(childIterator); } + } + } } private: @@ -209,17 +229,15 @@ export namespace CppUtils::Container for (auto& child : m_children) { - auto childWeak = WeakPtr{}; - if (const auto* shared = std::get_if(&child)) - childWeak = *shared; - else if (const auto* weak = std::get_if(&child)) - childWeak = *weak; + const auto* shared = std::get_if(&child); + if (not shared) + continue; - if (Thread::ownerEqual(childWeak, lockedParentWeak)) + if (Thread::ownerEqual(*shared, lockedParentWeak)) continue; - if (auto accessor = getChildAccessor(child)) - (*accessor)->updateDistance(m_weakThis, m_distanceFromRoot); + auto accessor = (*shared)->uniqueAccess(); + accessor->updateDistance(m_weakThis, m_distanceFromRoot); } } diff --git a/modules/Container/Sequence.mpp b/modules/Container/Sequence.mpp index a010d5fe..463885ac 100644 --- a/modules/Container/Sequence.mpp +++ b/modules/Container/Sequence.mpp @@ -1,6 +1,7 @@ export module CppUtils.Container.Sequence; import std; +import CppUtils.Type.Concept; export namespace CppUtils::Container::Sequence { @@ -12,52 +13,99 @@ export namespace CppUtils::Container::Sequence Edited }; - template - [[nodiscard]] inline auto getDifferences(std::span list0, std::span list1) -> std::vector + /** + * @brief Computes element-wise differences between two random-access ranges. + * + * Supports any random-access sized ranges (e.g. std::string, std::string_view, std::vector, std::span), + * optional custom element comparator, and projections for both ranges. + * + * @note Implements the classic dynamic programming Longest Common Subsequence (LCS) algorithm + * with O(N * M) time and space complexity. + * + * @see https://en.wikipedia.org/wiki/Longest_common_subsequence (Longest common subsequence) + * + * @tparam Range0 Type of the first range (must satisfy Type::SizedRandomAccessRange). + * @tparam Range1 Type of the second range (must satisfy Type::SizedRandomAccessRange). + * @tparam Comparator Binary predicate comparing elements. + * @tparam FirstProjection Projection applied to elements of the first range. + * @tparam SecondProjection Projection applied to elements of the second range. + * @param list0 First range to compare. + * @param list1 Second range to compare. + * @param comparator Custom comparison predicate (defaults to std::ranges::equal_to). + * @param firstProjection Projection callable for list0 (defaults to std::identity). + * @param secondProjection Projection callable for list1 (defaults to std::identity). + * @return Vector of Difference values detailing added, removed, or identical elements. + */ + template< + Type::SizedRandomAccessRange Range0, + Type::SizedRandomAccessRange Range1, + class Comparator = std::ranges::equal_to, + class FirstProjection = std::identity, + class SecondProjection = std::identity> + requires std::indirectly_comparable< + std::ranges::iterator_t, + std::ranges::iterator_t, + Comparator, + FirstProjection, + SecondProjection> + [[nodiscard]] constexpr auto getDifferences( + const Range0& list0, + const Range1& list1, + Comparator comparator = {}, + FirstProjection firstProjection = {}, + SecondProjection secondProjection = {}) -> std::vector { + const auto firstSize = static_cast(std::ranges::size(list0)); + const auto secondSize = static_cast(std::ranges::size(list1)); + const auto firstBegin = std::ranges::begin(list0); + const auto secondBegin = std::ranges::begin(list1); + + auto commonSubsequenceMatrix = std::vector((firstSize + 1uz) * (secondSize + 1uz), 0uz); + const auto matrixCell = [&](std::size_t firstCoordinate, std::size_t secondCoordinate) -> std::size_t& { + return commonSubsequenceMatrix[firstCoordinate * (secondSize + 1uz) + secondCoordinate]; + }; + + for (auto firstCoordinate = 1uz; firstCoordinate <= firstSize; ++firstCoordinate) + { + for (auto secondCoordinate = 1uz; secondCoordinate <= secondSize; ++secondCoordinate) + { + const auto firstOffset = static_cast(firstCoordinate - 1uz); + const auto secondOffset = static_cast(secondCoordinate - 1uz); + if (std::invoke(comparator, std::invoke(firstProjection, firstBegin[firstOffset]), std::invoke(secondProjection, secondBegin[secondOffset]))) + matrixCell(firstCoordinate, secondCoordinate) = matrixCell(firstCoordinate - 1uz, secondCoordinate - 1uz) + 1uz; + else + matrixCell(firstCoordinate, secondCoordinate) = std::max(matrixCell(firstCoordinate - 1uz, secondCoordinate), matrixCell(firstCoordinate, secondCoordinate - 1uz)); + } + } + auto differences = std::vector{}; - differences.reserve(std::ranges::max(std::ranges::size(list0), std::ranges::size(list1))); - auto i = 0uz, j = 0uz, diffLength = 0uz; - while (i < std::ranges::size(list0) and j < std::ranges::size(list1)) + differences.reserve(firstSize + secondSize); + + auto firstCoordinate = firstSize; + auto secondCoordinate = secondSize; + while (firstCoordinate > 0uz or secondCoordinate > 0uz) { - const auto& lhs = list0[i]; - const auto& rhs = list1[j]; - if (lhs == rhs) + const auto firstOffset = static_cast(firstCoordinate - 1uz); + const auto secondOffset = static_cast(secondCoordinate - 1uz); + if (firstCoordinate > 0uz and secondCoordinate > 0uz and std::invoke(comparator, std::invoke(firstProjection, firstBegin[firstOffset]), std::invoke(secondProjection, secondBegin[secondOffset]))) { - if (diffLength > 0) - { - for (auto k = 0uz; k < diffLength; ++k) - differences.emplace_back(Difference::Removed); - for (; diffLength; --diffLength) - differences.emplace_back(Difference::Added); - } differences.emplace_back(Difference::None); + --firstCoordinate; + --secondCoordinate; } - else if (diffLength > 0 and list0[i - diffLength] == rhs) + else if (secondCoordinate > 0uz and (firstCoordinate == 0uz or matrixCell(firstCoordinate, secondCoordinate - 1uz) >= matrixCell(firstCoordinate - 1uz, secondCoordinate))) { - i -= diffLength; - for (; diffLength; --diffLength) - differences.emplace_back(Difference::Added); - differences.emplace_back(Difference::None); + differences.emplace_back(Difference::Added); + --secondCoordinate; } - else if (diffLength > 0 and lhs == list1[j - diffLength]) + else if (firstCoordinate > 0uz and (secondCoordinate == 0uz or matrixCell(firstCoordinate, secondCoordinate - 1uz) < matrixCell(firstCoordinate - 1uz, secondCoordinate))) { - j -= diffLength; - for (; diffLength; --diffLength) - differences.emplace_back(Difference::Removed); - differences.emplace_back(Difference::None); + differences.emplace_back(Difference::Removed); + --firstCoordinate; } - else - ++diffLength; - ++i; - ++j; } - auto offsetList0 = std::ranges::size(list0) - i; - while (offsetList0--) - differences.emplace_back(Difference::Removed); - auto offsetList1 = std::ranges::size(list1) - j; - while (offsetList1--) - differences.emplace_back(Difference::Added); + + std::ranges::reverse(differences); return differences; } } diff --git a/modules/Execution/DoNotOptimize.mpp b/modules/Execution/DoNotOptimize.mpp new file mode 100644 index 00000000..20e92117 --- /dev/null +++ b/modules/Execution/DoNotOptimize.mpp @@ -0,0 +1,26 @@ +export module CppUtils.Execution.DoNotOptimize; + +import std; + +export namespace CppUtils::Execution +{ + /** + * @brief Prevents compiler optimization (dead code elimination) for an expression or variable. + * + * Uses an inline assembly memory barrier on Clang/GCC to force the compiler + * to retain the evaluated value in registers or memory. + * + * @param value Forwarding reference to the value or expression to keep alive. + */ + inline auto doNotOptimize(auto&& value) -> void + { +#if defined(__clang__) || defined(__GNUC__) + if constexpr (std::is_const_v>) + asm volatile("" : : "r,m"(value) : "memory"); + else + asm volatile("" : "+r,m"(value) : : "memory"); +#else + std::atomic_signal_fence(std::memory_order_seq_cst); +#endif + } +} diff --git a/modules/Execution/Execution.mpp b/modules/Execution/Execution.mpp index 048885d6..43aae300 100644 --- a/modules/Execution/Execution.mpp +++ b/modules/Execution/Execution.mpp @@ -7,3 +7,4 @@ export import CppUtils.Execution.Pipeline; export import CppUtils.Execution.Retry; export import CppUtils.Execution.ScopeGuard; export import CppUtils.Execution.TryExecute; +export import CppUtils.Execution.DoNotOptimize; diff --git a/modules/FileSystem/Directory.mpp b/modules/FileSystem/Directory.mpp index 981f0365..392c6883 100644 --- a/modules/FileSystem/Directory.mpp +++ b/modules/FileSystem/Directory.mpp @@ -10,8 +10,7 @@ export namespace CppUtils::FileSystem auto&& function, bool recursively = false) -> void { - if (not std::filesystem::exists(directoryPath) or - not std::filesystem::is_directory(directoryPath)) + if (not std::filesystem::exists(directoryPath) or not std::filesystem::is_directory(directoryPath)) return; if (recursively) { diff --git a/modules/Language/INI/IniLexer.mpp b/modules/Language/INI/IniLexer.mpp index 0f2eadf5..f68e500e 100644 --- a/modules/Language/INI/IniLexer.mpp +++ b/modules/Language/INI/IniLexer.mpp @@ -52,8 +52,7 @@ export namespace CppUtils::Language::Ini [[nodiscard]] static inline auto identifierParser(Lexer::Context& context, char delimiter) -> bool { auto& [cursor, parentNode, firstChildPosition, parsingErrors] = context; - const auto string = std::string{String::trimString(String::getStringAndSkipIf(cursor, [delimiter](char character) -> bool - { + const auto string = std::string{String::trimString(String::getStringAndSkipIf(cursor, [delimiter](char character) -> bool { return character != delimiter and character != ';' and character != '[' and character != ']' and (std::isspace(static_cast(character)) or std::isgraph(static_cast(character))); }))}; if (std::ranges::empty(string)) diff --git a/modules/Language/Language.mpp b/modules/Language/Language.mpp index ec42f5cb..76d3b380 100644 --- a/modules/Language/Language.mpp +++ b/modules/Language/Language.mpp @@ -18,6 +18,7 @@ export import CppUtils.Language.Xml.Pipeline; export import CppUtils.Language.Xml.Tags; export import CppUtils.Language.Xml.Flow; export import CppUtils.Language.Xml.FlowRunner; +export import CppUtils.Language.Xml.Utility; export import CppUtils.Language.Ini; export import CppUtils.Language.Parameters; export import CppUtils.Language.Markdown; diff --git a/modules/Language/Lexer/AstLexer.mpp b/modules/Language/Lexer/AstLexer.mpp index de041970..fae64cd4 100644 --- a/modules/Language/Lexer/AstLexer.mpp +++ b/modules/Language/Lexer/AstLexer.mpp @@ -29,10 +29,8 @@ export namespace CppUtils::Language::Lexer std::vector>& memory) -> bool { for (const auto& node : lexeme.nodes) - { if (not parseLexeme(cursor, parentNode, ast, node, grammar, memory)) return false; - } return true; } @@ -164,10 +162,8 @@ export namespace CppUtils::Language::Lexer case "or"_token: { for (const auto& node : lexeme.nodes) - { if (parseLexeme(cursor, parentNode, ast, node, grammar, memory)) return true; - } return false; } case "hash"_token: @@ -298,7 +294,10 @@ export namespace CppUtils::Language::Lexer const auto success = parseDeclaration(cursor, parentRef, ast, mainLexeme, grammar, memory); if (not success or not String::isEndOfString(cursor)) throw std::logic_error{std::format("Parsing failed: success={}, pos={}/{}, remaining: \"{}\"", - success, cursor.position, cursor.source.size(), cursor.source.substr(cursor.position))}; + success, + cursor.position, + cursor.source.size(), + cursor.source.substr(cursor.position))}; return ast; } } diff --git a/modules/Language/Lexer/Engine.mpp b/modules/Language/Lexer/Engine.mpp index ec59537f..77496cb9 100644 --- a/modules/Language/Lexer/Engine.mpp +++ b/modules/Language/Lexer/Engine.mpp @@ -278,8 +278,7 @@ export namespace CppUtils::Language::Lexer break; ++i; } - if ((recurrenceType == RecurrenceType::MoreThan and i <= repetitions) or - (recurrenceType == RecurrenceType::MoreOrEqualTo and i < repetitions)) + if ((recurrenceType == RecurrenceType::MoreThan and i <= repetitions) or (recurrenceType == RecurrenceType::MoreOrEqualTo and i < repetitions)) { parsingErrors.push_back(ParsingError{ .cursorPosition = cursor.position, diff --git a/modules/Language/Parameters/ParametersLexer.mpp b/modules/Language/Parameters/ParametersLexer.mpp index 54557645..12524558 100644 --- a/modules/Language/Parameters/ParametersLexer.mpp +++ b/modules/Language/Parameters/ParametersLexer.mpp @@ -18,14 +18,17 @@ export namespace CppUtils::Language::Parameters using namespace String::Literals; m_grammarLexer.addParsingFunction("spaceParser"_token, Lexer::spaceParser); - m_grammarLexer.addParsingFunction("keywordParser"_token, stringKeywordParser); - m_grammarLexer.addParsingFunction("contentParser"_token, contentParser); + m_grammarLexer.addParsingFunction("keyParser"_token, keyParser); + m_grammarLexer.addParsingFunction("quoteParser"_token, Lexer::quoteParser); + m_grammarLexer.addParsingFunction("unquotedValueParser"_token, unquotedValueParser); static constexpr auto grammarSrc = R"( main: spaceParser (command >= 0) spaceParser; - command: spaceParser [key] ~value; - !key: keywordParser; - !value: spaceParser '[' contentParser ']'; + command: spaceParser ~prefix [key] ~assignment; + !prefix: ("--" || '-'); + !key: keyParser; + !assignment: '=' value; + !value: (quoteParser || unquotedValueParser); )"sv; m_grammarLexer.parseGrammar(grammarSrc); } @@ -61,12 +64,7 @@ export namespace CppUtils::Language::Parameters { if (argc <= 1 or argv == nullptr) return {}; - auto args = std::vector{}; - args.reserve(static_cast(argc - 1)); - for (auto i = 1; i < argc; ++i) - if (argv[i] != nullptr) - args.emplace_back(argv[i]); - return parseParameters(args); + return parseParameters(std::vector{argv + 1, argv + argc}); } [[nodiscard]] inline auto executeCommands( @@ -96,33 +94,29 @@ export namespace CppUtils::Language::Parameters { if (argc <= 1 or argv == nullptr) return false; - auto args = std::vector{}; - args.reserve(static_cast(argc - 1)); - for (auto i = 1; i < argc; ++i) - if (argv[i] != nullptr) - args.emplace_back(argv[i]); - return executeCommands(args, commands); + return executeCommands(std::vector{argv + 1, argv + argc}, commands); } private: - [[nodiscard]] static inline auto stringKeywordParser(Lexer::Context& context) -> bool + [[nodiscard]] static inline auto keyParser(Lexer::Context& context) -> bool { auto& [cursor, parentNode, firstChildPosition, parsingErrors] = context; - const auto keyword = String::getKeywordAndSkip(cursor); - if (std::ranges::empty(keyword)) + const auto key = std::string{String::getStringAndSkipIf(cursor, [](char character) -> bool { + return std::isalnum(static_cast(character)) or character == '_' or character == '-'; + })}; + if (std::ranges::empty(key)) return false; - parentNode.get().nodes.emplace_back(std::string{keyword}); + parentNode.get().nodes.emplace_back(key); return true; } - [[nodiscard]] static inline auto contentParser(Lexer::Context& context) -> bool + [[nodiscard]] static inline auto unquotedValueParser(Lexer::Context& context) -> bool { auto& [cursor, parentNode, firstChildPosition, parsingErrors] = context; - const auto string = std::string{String::trimString(String::getStringAndSkipIf(cursor, [](char character) -> bool - { - return character != ']'; + const auto value = std::string{String::trimString(String::getStringAndSkipIf(cursor, [](char character) -> bool { + return not std::isspace(static_cast(character)); }))}; - parentNode.get().nodes.emplace_back(string); + parentNode.get().nodes.emplace_back(value); return true; } diff --git a/modules/Language/Parser/AstParser.mpp b/modules/Language/Parser/AstParser.mpp index b554a51e..c551d0e8 100644 --- a/modules/Language/Parser/AstParser.mpp +++ b/modules/Language/Parser/AstParser.mpp @@ -20,7 +20,10 @@ export namespace CppUtils::Language::Parser ++cursor.position; switch (value) { - case ' ': case '\n': case '\t': case '\r': + case ' ': + case '\n': + case '\t': + case '\r': tokenNameBuffer.clear(); break; case '{': diff --git a/modules/Language/XML/Flow.mpp b/modules/Language/XML/Flow.mpp index 2497233f..bf7b0620 100644 --- a/modules/Language/XML/Flow.mpp +++ b/modules/Language/XML/Flow.mpp @@ -275,7 +275,7 @@ export namespace CppUtils::Language::Xml std::optional m_outputFile; std::optional m_outputDirectory; - std::string m_outputFormat = "csv"; + [[maybe_unused]] std::string m_outputFormat = "csv"; std::string m_outputSeparator = ";"; std::optional m_rejectedFile; diff --git a/modules/Language/XML/Utility.mpp b/modules/Language/XML/Utility.mpp new file mode 100644 index 00000000..c94097c3 --- /dev/null +++ b/modules/Language/XML/Utility.mpp @@ -0,0 +1,33 @@ +export module CppUtils.Language.Xml.Utility; + +import std; + +export namespace CppUtils::Language::Xml +{ + /** + * @brief Escapes reserved XML characters (`&`, `<`, `>`, `"`, `'`). + * @param text Raw input text. + * @return XML-escaped string. + */ + [[nodiscard]] inline auto escapeXml(std::string_view text) -> std::string + { + auto result = std::string{}; + result.reserve(std::ranges::size(text)); + for (const auto character : text) + { + switch (character) + { + case '&': result += "&"; break; + case '<': result += "<"; break; + case '>': result += ">"; break; + case '"': result += """; break; + case '\'': result += "'"; break; + default: + if (static_cast(character) >= 0x20 or character == '\t' or character == '\n' or character == '\r') + result += character; + break; + } + } + return result; + } +} diff --git a/modules/Language/XML/XmlLexer.mpp b/modules/Language/XML/XmlLexer.mpp index 506abad2..c029026e 100644 --- a/modules/Language/XML/XmlLexer.mpp +++ b/modules/Language/XML/XmlLexer.mpp @@ -52,8 +52,7 @@ export namespace CppUtils::Language::Xml [[nodiscard]] static inline auto textParser(Lexer::Context& context) -> bool { auto& [cursor, parentNode, firstChildPosition, parsingErrors] = context; - const auto string = std::string{String::trimString(String::getStringAndSkipIf(cursor, [](char character) -> bool - { + const auto string = std::string{String::trimString(String::getStringAndSkipIf(cursor, [](char character) -> bool { return character != '<'; }))}; if (std::ranges::empty(string)) diff --git a/modules/Log/Config/CppUtils.mpp b/modules/Log/Config/CppUtils.mpp index bba1836b..2f19acb2 100644 --- a/modules/Log/Config/CppUtils.mpp +++ b/modules/Log/Config/CppUtils.mpp @@ -51,8 +51,16 @@ export namespace CppUtils }); eventQueue.subscribe<"error">([](const std::string& message) { - auto _ = Terminal::TextModifier{stdout, Terminal::TextColor::TextColorEnum::Red}; - std::println("{:^{}}\n{} ✕", "🛇 ERROR 🛇", Terminal::getTerminalSize().width(), message); + { + auto _ = Terminal::TextModifier{stdout, Terminal::TextColor::TextColorEnum::Red}; + std::println("{:^{}}", "🛇 ERROR 🛇", Terminal::getTerminalSize().width()); + } + std::println("{}\n", message); + { + auto _ = Terminal::TextModifier{stdout, Terminal::TextColor::TextColorEnum::Red}; + std::println("Failed ✕"); + } + std::fflush(stdout); }); eventQueue.subscribe<"color">([](Terminal::TextColor::TextColorEnum color, const std::string& message) { diff --git a/modules/Log/Config/UnitTests.mpp b/modules/Log/Config/UnitTests.mpp index 4bf1a54d..774d2778 100644 --- a/modules/Log/Config/UnitTests.mpp +++ b/modules/Log/Config/UnitTests.mpp @@ -46,8 +46,10 @@ export namespace CppUtils }); eventQueue.subscribe<"detail">([](const std::string& message) { - auto _ = Terminal::TextModifier{stdout, Terminal::TextColor::TextColorEnum::Blue}; - std::println("{}", message); + { + auto _ = Terminal::TextModifier{stdout, Terminal::TextColor::TextColorEnum::Blue}; + std::println("{}", message); + } std::fflush(stdout); }); @@ -58,8 +60,15 @@ export namespace CppUtils }); eventQueue.subscribe<"error">([](const std::string& message) { - auto _ = Terminal::TextModifier{stdout, Terminal::TextColor::TextColorEnum::Red}; - std::println("{:^{}}\n{} ✕", "🛇 ERROR 🛇", Terminal::getTerminalSize().width(), message); + { + auto _ = Terminal::TextModifier{stdout, Terminal::TextColor::TextColorEnum::Red}; + std::println("{:^{}}", "🛇 ERROR 🛇", Terminal::getTerminalSize().width()); + } + std::println("{}", message); + { + auto _ = Terminal::TextModifier{stdout, Terminal::TextColor::TextColorEnum::Red}; + std::println("Failed ✕"); + } std::fflush(stdout); }); diff --git a/modules/Log/Logger.mpp b/modules/Log/Logger.mpp index 56b16d45..83e018b8 100644 --- a/modules/Log/Logger.mpp +++ b/modules/Log/Logger.mpp @@ -33,6 +33,12 @@ export namespace CppUtils return eventQueue; } + static inline auto reset() -> void + { + Pattern::Multiton::reset(); + configure(Pattern::Multiton::get()); + } + static inline auto configure(Execution::EventQueue& eventQueue) -> void { configureLogger(eventQueue); diff --git a/modules/Math/Random.mpp b/modules/Math/Random.mpp index 75112d59..220e8c0f 100644 --- a/modules/Math/Random.mpp +++ b/modules/Math/Random.mpp @@ -1,6 +1,7 @@ export module CppUtils.Math.Random; import std; +import CppUtils.Type.Hash; export namespace CppUtils::Math { @@ -15,4 +16,223 @@ export namespace CppUtils::Math auto randomNumber = distribution(engine); return randomNumber; } + + /** + * @brief Deterministic, constexpr-compatible pseudo-random number generator (SplitMix64). + * + * Provides reproducible pseudo-random generation suitable for unit tests, fuzz testing, + * and property-based verification. Generates identical sequences across platforms for a given seed. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#générateurs-pseudo-aléatoires-disponibles (Guide des tests unitaires) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#available-pseudo-random-generators (Unit test guide) + */ + class DeterministicRandomEngine final + { + public: + /// @brief Default 64-bit seed used for deterministic pseudo-random initialization. + static constexpr auto defaultSeed = 0x123456789ABCDEF0ULL; + + /** + * @brief Constructs a deterministic random engine with a given 64-bit seed. + * @param seed Initial seed (defaults to defaultSeed). + */ + explicit constexpr DeterministicRandomEngine(std::uint64_t seed = defaultSeed) noexcept: + m_initialSeed{seed}, + m_state{seed != 0 ? seed : 0x853C49E6748FEA9BULL} + {} + + /** + * @brief Returns the initial seed configured for this engine. + * @return 64-bit unsigned integer seed. + */ + [[nodiscard]] constexpr auto seed() const noexcept -> std::uint64_t + { + return m_initialSeed; + } + + /* + Générateur pseudo-aléatoire SplitMix64 : + https://en.wikipedia.org/wiki/Xorshift#Initialization + https://en.wikipedia.org/wiki/Weyl_sequence + La constante 0x9E3779B97F4A7C15ULL est l'incrément de Weyl basé sur le ratio d'or: + floor(2^64 / phi) = floor(2^64 * (sqrt(5) - 1) / 2) = 11400714819323198485 = 0x9e3779b97f4a7c15. + Cette valeur impaire est première avec 2^64 et assure une période maximale de 2^64 avec + une équidistribution optimale. Le ratio d'or offre les meilleures propriétés de dispersion + et d'évitement des résonances harmoniques parmi tous les réels irrationnels (théorème des trois distances de Weyl). + */ + /** + * @brief Advances the generator state and produces the next raw 64-bit pseudo-random integer. + * @return Next pseudo-random std::uint64_t. + */ + constexpr auto nextUint64() noexcept -> std::uint64_t + { + m_state += 0x9E3779B97F4A7C15ULL; + return Type::mixHash(m_state); + } + + /** + * @brief Generates a pseudo-random value within the specified range [minimum, maximum]. + * + * Deduce the type automatically from parameters or specify it explicitly. + * Supports integral types, floating-point types, and booleans. + * + * @tparam Type Arithmetic or boolean type. + * @param minimum Lower bound (inclusive). + * @param maximum Upper bound (inclusive). + * @return Random value within [minimum, maximum]. + */ + template + constexpr auto random(Type minimum, Type maximum) noexcept -> Type + { + if (minimum >= maximum) + return minimum; + if constexpr (std::same_as) + { + return (nextUint64() & 1ULL) != 0; + } + else if constexpr (std::integral) + { + using Unsigned = std::make_unsigned_t; + const auto difference = static_cast(maximum) - static_cast(minimum); + if constexpr (sizeof(Type) == sizeof(std::uint64_t)) + { + if (difference == std::numeric_limits::max()) + return static_cast(nextUint64()); + } + const auto range = static_cast(difference) + 1ULL; + return static_cast(minimum + static_cast(nextUint64() % range)); + } + else if constexpr (std::floating_point) + { + constexpr auto factor = 1.0 / static_cast(std::numeric_limits::max()); + const auto normalized = static_cast(nextUint64()) * factor; + return static_cast(minimum + normalized * (maximum - minimum)); + } + } + + /** + * @brief Generates a pseudo-random value of the requested type covering its standard range. + * + * For integral types, covers [`std::numeric_limits::min()`, `std::numeric_limits::max()`]. + * For floating-point types, covers [0.0, 1.0]. + * For bool, returns true or false with a balanced 50% distribution. + * + * @tparam Type Target arithmetic or boolean type. + * @return Random value of type Type. + */ + template + constexpr auto random() noexcept -> Type + { + if constexpr (std::same_as) + return (nextUint64() & 1ULL) != 0; + else if constexpr (std::integral) + return random(std::numeric_limits::min(), std::numeric_limits::max()); + else if constexpr (std::floating_point) + return random(static_cast(0.0), static_cast(1.0)); + } + + /** + * @brief Generates a floating-point number in [minimum, maximum] (defaults: 0.0 to 1.0). + * @tparam Float Floating-point type. + * @param minimum Lower bound (inclusive). + * @param maximum Upper bound (inclusive). + * @return Random floating-point value. + */ + template + constexpr auto randomFloat(Float minimum = static_cast(0.0), Float maximum = static_cast(1.0)) noexcept -> Float + { + return random(minimum, maximum); + } + + /** + * @brief Generates a pseudo-random size within the specified bounds. + * @param minimum Lower bound (inclusive). + * @param maximum Upper bound (inclusive). + * @return Random std::size_t in [minimum, maximum]. + */ + constexpr auto randomSize(std::size_t minimum, std::size_t maximum) noexcept -> std::size_t + { + return random(minimum, maximum); + } + + /** + * @brief Generates a pseudo-random size between 0 and maximum (inclusive). + * @param maximum Upper bound (inclusive). + * @return Random std::size_t in [0, maximum]. + */ + constexpr auto randomSize(std::size_t maximum) noexcept -> std::size_t + { + return random(0uz, maximum); + } + + /** + * @brief Generates a pseudo-random size bounded by PTRDIFF_MAX. + * + * The generated size is bounded within [0, `std::numeric_limits::max()`] + * (PTRDIFF_MAX) instead of SIZE_MAX, ensuring that the size never exceeds the maximum + * legal contiguous object size in C++, thereby preserving the validity of pointer arithmetic + * ([expr.add#5]) during memory allocation and container stress testing. + * + * @see https://morgancaron.github.io/articles/cpp/size#limitation-de-la-plage-de-valeurs (Limitation de la plage de valeurs et borne PTRDIFF_MAX) + * + * @return Random std::size_t in [0, PTRDIFF_MAX]. + */ + constexpr auto randomSize() noexcept -> std::size_t + { + return random(0uz, static_cast(std::numeric_limits::max())); + } + + /** + * @brief Generates a pseudo-random string of variable length composed of characters from the given alphabet. + * @param minimumLength Minimum number of characters. + * @param maximumLength Maximum number of characters. + * @param alphabet View of available characters to sample from. + * @return Randomly generated std::string. + */ + constexpr auto randomString( + std::size_t minimumLength = 0uz, + std::size_t maximumLength = 32uz, + std::string_view alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ -") -> std::string + { + const auto length = random(minimumLength, maximumLength); + auto result = std::string{}; + result.reserve(length); + for (auto _ : std::views::iota(0uz, length)) + { + const auto characterIndex = random(0uz, std::ranges::size(alphabet) - 1uz); + result += alphabet[characterIndex]; + } + return result; + } + + /** + * @brief Allocates and fills a vector of bytes with pseudo-random data. + * @param count Number of bytes to generate. + * @return Vector of random bytes. + */ + constexpr auto randomBytes(std::size_t count) -> std::vector + { + auto result = std::vector(count); + for (auto& byte : result) + byte = static_cast(nextUint64() & 0xFF); + return result; + } + + /** + * @brief Picks a pseudo-random element by const reference from a random access range. + * @tparam Range Range satisfying std::ranges::random_access_range. + * @param collection Collection to pick an element from. + * @return Const reference to the randomly selected element. + */ + template + constexpr auto pickOne(const Range& collection) -> const auto& + { + const auto index = random(0uz, std::ranges::size(collection) - 1uz); + return collection[index]; + } + + private: + std::uint64_t m_initialSeed; + std::uint64_t m_state; + }; } diff --git a/modules/Math/Utility.mpp b/modules/Math/Utility.mpp index 6e223a64..8f49de5c 100644 --- a/modules/Math/Utility.mpp +++ b/modules/Math/Utility.mpp @@ -27,6 +27,34 @@ export namespace CppUtils::Math static_assert(absolute(0u) == 0u); static_assert(absolute(1u) == 1u); + /** + * @brief Performs robust, magnitude-adaptive floating-point equality comparison. + * + * Direct comparison of floating-point numbers with `operator==` is notoriously unreliable + * due to IEEE 754 binary representation errors and rounding artifacts. For instance, the expression + * `(0.1 + 0.2) == 0.3` evaluates to `false` because in base-2 floating point, `0.1 + 0.2` results + * in `0.30000000000000004440892098500626...`. + * + * Furthermore, naive approaches like `std::abs(a - b) < fixed_epsilon` fail for two major reasons: + * 1. For large numbers, a fixed epsilon is far too tight (the gap between representable numbers + * exceeds the epsilon), causing false negatives. + * 2. Near zero (subnormal numbers), relative scaling collapses, causing underflow issues. + * + * `isEqual` solves both problems by implementing an adaptive tolerance algorithm: + * `|lhs - rhs| <= std::max(minimumEpsilon, epsilon * std::max(|lhs|, |rhs|))` + * + * - **Adaptive scaling**: Scales tolerance proportionally to the operands' magnitude (`epsilon * max`), + * maintaining accurate relative precision across small and arbitrarily large numbers. + * - **Zero-proximity fallback**: Protects comparisons near zero by falling back to `minimumEpsilon` + * (`std::numeric_limits::min()` by default). + * + * @tparam Float A floating-point type satisfying `std::floating_point`. + * @param lhs Left-hand operand to compare. + * @param rhs Right-hand operand to compare. + * @param epsilon Relative tolerance factor (defaults to machine epsilon `std::numeric_limits::epsilon()`). + * @param minimumEpsilon Absolute tolerance floor for values close to zero (defaults to `std::numeric_limits::min()`). + * @return `true` if the difference between operands falls within the adaptive tolerance, `false` otherwise. + */ template [[nodiscard]] inline constexpr auto isEqual( Float lhs, diff --git a/modules/Pattern/Multiton.mpp b/modules/Pattern/Multiton.mpp index 483e0d3f..f698ce8d 100644 --- a/modules/Pattern/Multiton.mpp +++ b/modules/Pattern/Multiton.mpp @@ -20,10 +20,19 @@ export namespace CppUtils::Pattern class Multiton final { public: - [[nodiscard]] static inline auto get() noexcept -> T& + [[nodiscard]] static inline auto& get() noexcept { - static auto instance = T{}; - return instance; + auto& instancePtr = getInstancePtr(); + if (not instancePtr) + instancePtr = std::make_unique(); + return *instancePtr; + } + + static inline auto reset() -> void + { + auto& instancePtr = getInstancePtr(); + instancePtr.release(); + instancePtr = std::make_unique(); } Multiton() = default; @@ -32,5 +41,12 @@ export namespace CppUtils::Pattern auto operator=(const Multiton&) -> Multiton& = delete; auto operator=(Multiton&&) -> Multiton& = delete; ~Multiton() = default; + + private: + [[nodiscard]] static inline auto& getInstancePtr() noexcept + { + static auto instancePtr = std::make_unique(); + return instancePtr; + } }; } diff --git a/modules/String/Concept.mpp b/modules/String/Concept.mpp index 63d9c449..6e07ac59 100644 --- a/modules/String/Concept.mpp +++ b/modules/String/Concept.mpp @@ -3,12 +3,10 @@ export module CppUtils.String.Concept; import std; import CppUtils.Type.Concept; +import CppUtils.Type.Specialization; export namespace CppUtils::String { - template - concept Char = Type::IsOneOf; - template concept String = Type::Specializes; @@ -24,8 +22,90 @@ export namespace CppUtils::String template using TextCharT = typename decltype(std::basic_string_view{std::declval()})::value_type; + /// @brief Concept satisfied if T is a raw pointer or array of character types. + template + concept CharacterPointerOrArray = + (std::is_pointer_v> or std::is_array_v>) and Type::Character>; + + /// @brief Resolves the decayed character element type of a pointer, array, or character type. template - concept Formattable = requires (T t) { - { std::formatter{} }; + requires Type::Character> + using CharacterType = CppUtils::Type::UnderlyingElementType; + + /// @brief Concept satisfied by types representing contiguous character sequences (strings, string views, or spans of characters). + template + concept CharacterSequence = Text or requires (const T& target) { + { std::span{target} }; }; + + template + concept Formattable = std::formattable, char>; + + /// @brief Concept satisfied if T is directly formattable as text without structural decomposition. + template + concept DirectlyPrintable = + (std::formattable, char> + or std::same_as, std::byte>) + and (not std::ranges::input_range> + or GenericText> + or std::same_as, std::filesystem::path>); + + template + inline consteval auto isExtensivelyPrintable() -> bool + { + using CleanType = std::remove_cvref_t; + + if constexpr (DirectlyPrintable) + { + return true; + } + else if constexpr (std::ranges::input_range and not GenericText) + { + if constexpr (not std::same_as, CleanType>) + return isExtensivelyPrintable>(); + else + return false; + } + else if constexpr (requires { std::variant_size::value; }) + { + return [](std::index_sequence) { + return (isExtensivelyPrintable>() and ...); + }(std::make_index_sequence>{}); + } + else if constexpr (requires { typename std::tuple_size::type; } and not std::ranges::input_range) + { + return [](std::index_sequence) { + return (isExtensivelyPrintable>() and ...); + }(std::make_index_sequence>{}); + } + else if constexpr (requires (const CleanType& target) { static_cast(target); *target; } + and not GenericText) + { + using DereferencedType = std::remove_cvref_t())>; + if constexpr (not std::same_as) + return isExtensivelyPrintable(); + else + return false; + } + else + { + return false; + } + } + + /// @brief Concept satisfied if T can be formatted directly or through recursive decomposition. + template + concept ExtensivelyPrintable = isExtensivelyPrintable(); } + +#if not defined(__cpp_lib_format_path) +template +struct std::formatter: std::formatter, CharT> +{ + template + auto format(const std::filesystem::path& path, FormatContext& context) const + { + return std::formatter, CharT>::format(path.string(), context); + } +}; +#endif diff --git a/modules/String/Hash.mpp b/modules/String/Hash.mpp index f2f730e1..6262444b 100644 --- a/modules/String/Hash.mpp +++ b/modules/String/Hash.mpp @@ -3,11 +3,12 @@ export module CppUtils.String.Hash; import std; import CppUtils.String.Concept; +import CppUtils.Type.Concept; export import CppUtils.Type.Hash; export namespace CppUtils::String { - template + template struct Hasher final { inline constexpr Hasher(Type::Hash hash): diff --git a/modules/String/HashTable.mpp b/modules/String/HashTable.mpp index 3e1826a9..f45db1a9 100644 --- a/modules/String/HashTable.mpp +++ b/modules/String/HashTable.mpp @@ -5,19 +5,20 @@ import std; import CppUtils.String.Concept; import CppUtils.String.Hash; import CppUtils.String.Utility; +import CppUtils.Type.Concept; export namespace CppUtils::String { - template + template using HashTable = std::unordered_map>; - template + template [[nodiscard]] inline auto makeHashTable(Text auto&&... texts) -> decltype(auto) { return HashTable{std::make_pair(Type::hash(texts), std::forward(texts))...}; } - template + template [[nodiscard]] inline auto getNameOrValue(Type::Token token, const HashTable& tokenNames) -> auto { using namespace std::literals; diff --git a/modules/String/Utility.mpp b/modules/String/Utility.mpp index d7323844..26bfb724 100644 --- a/modules/String/Utility.mpp +++ b/modules/String/Utility.mpp @@ -3,6 +3,7 @@ export module CppUtils.String.Utility; import std; import CppUtils.Math; +import CppUtils.Type.Concept; import CppUtils.Type.Specialization; import CppUtils.String.Concept; @@ -97,28 +98,28 @@ export namespace CppUtils::String return rightTrimString(leftTrimString(stringView)); } - [[nodiscard]] inline auto getPrintableChar(Char auto c) -> std::string + [[nodiscard]] inline auto getPrintableChar(Type::Character auto c) -> std::string { using namespace std::literals; return std::isprint(c) ? ("'"s + c + '\'') : std::to_string(c); } - [[nodiscard]] inline constexpr auto isUppercase(Char auto c) noexcept -> bool + [[nodiscard]] inline constexpr auto isUppercase(Type::Character auto c) noexcept -> bool { return (c >= 'A' and c <= 'Z'); } - [[nodiscard]] inline constexpr auto isLowercase(Char auto c) noexcept -> bool + [[nodiscard]] inline constexpr auto isLowercase(Type::Character auto c) noexcept -> bool { return (c >= 'a' and c <= 'z'); } - [[nodiscard]] inline constexpr auto toLowercase(Char auto c) noexcept -> decltype(c) + [[nodiscard]] inline constexpr auto toLowercase(Type::Character auto c) noexcept -> decltype(c) { return static_cast(isUppercase(c) ? c + ('a' - 'A') : c); } - [[nodiscard]] inline constexpr auto toUppercase(Char auto c) noexcept -> decltype(c) + [[nodiscard]] inline constexpr auto toUppercase(Type::Character auto c) noexcept -> decltype(c) { return static_cast(isLowercase(c) ? c - ('a' - 'A') : c); } @@ -145,17 +146,109 @@ export namespace CppUtils::String [[nodiscard]] inline auto formatValue(const auto& value) -> std::string { - using DecayedT = std::remove_cvref_t; - if constexpr (Type::Specializes) - return std::visit([](const auto& value) -> std::string { - return formatValue(value); - }, value); - else if constexpr (std::formattable) - return std::format("{}", value); - else if constexpr (requires { value.string(); }) - return value.string(); + using namespace std::literals; + using DecayedType = std::remove_cvref_t; + + const auto formatSubElement = [](const auto& element) -> std::string { + using ElementType = std::remove_cvref_t; + if constexpr (GenericText) + return std::format("\"{}\"", element); + else if constexpr (Type::Character) + return std::format("'{}'", element); + else + return formatValue(element); + }; + + if constexpr (requires { std::variant_size::value; }) + { + if constexpr (ExtensivelyPrintable) + { + return std::visit([](const auto& alternative) -> std::string { + return formatValue(alternative); + }, value); + } + else + { + return ""; + } + } + else if constexpr (requires { typename std::tuple_size::type; } and not std::ranges::input_range) + { + if constexpr (ExtensivelyPrintable) + { + auto formattedString = "("s; + auto isFirst = true; + std::apply([&](const auto&... elements) { + const auto formatElement = [&](const auto& element) { + if (not isFirst) + formattedString += ", "; + isFirst = false; + formattedString += formatSubElement(element); + }; + (formatElement(elements), ...); + }, value); + formattedString += ")"; + return formattedString; + } + else + { + return ""; + } + } + else if constexpr (DirectlyPrintable) + { + if constexpr (std::same_as) + return std::format("0x{:02X}", std::to_integer(value)); + else + return std::format("{}", value); + } + else if constexpr (requires { static_cast(value); *value; } and not GenericText) + { + using DereferencedType = std::remove_cvref_t; + if constexpr (ExtensivelyPrintable) + { + if (not static_cast(value)) + { + if constexpr (std::is_pointer_v or std::same_as or requires { value.get(); }) + return "nullptr"; + else if constexpr (requires { value.error(); }) + return std::format("", formatValue(value.error())); + else + return "nullopt"; + } + return formatValue(*value); + } + else + { + return ""; + } + } + else if constexpr (std::ranges::input_range and not GenericText) + { + if constexpr (not std::same_as, DecayedType> + and ExtensivelyPrintable>) + { + auto formattedString = std::string{"["}; + auto isFirst = true; + for (const auto& element : value) + { + if (not isFirst) + formattedString += ", "; + isFirst = false; + formattedString += formatSubElement(element); + } + formattedString += "]"; + return formattedString; + } + else + { + return ""; + } + } else + { return ""; + } } [[nodiscard]] inline constexpr auto toAscii(const GenericText auto& string) -> std::string diff --git a/modules/System/Pipe.mpp b/modules/System/Pipe.mpp index caa3356e..e2589626 100644 --- a/modules/System/Pipe.mpp +++ b/modules/System/Pipe.mpp @@ -3,21 +3,27 @@ module; #include #if defined(OS_LINUX) +# include # include # include // For O_* macros +# include #endif export module CppUtils.System.Pipe; import std; +import CppUtils.Chrono.Concept; import CppUtils.System.Error; import CppUtils.System.FileDescriptor; +import CppUtils.Type.Concept; export namespace CppUtils::System { #if defined(OS_LINUX) + /// @brief Low-level RAII pair of non-blocking pipe file descriptors. struct Pipe final { + /// @brief Constructs a non-blocking UNIX pipe pair via pipe2(O_NONBLOCK). inline Pipe() { auto pipeFileDescriptors = std::array{}; @@ -28,8 +34,205 @@ export namespace CppUtils::System write = pipeFileDescriptors[1]; } - FileDescriptor read; - FileDescriptor write; + FileDescriptor read; ///< Reading end file descriptor. + FileDescriptor write; ///< Writing end file descriptor. + }; + + /// @brief High-level RAII unidirectional anonymous IPC pipe channel with typed operations and timeout polling. + class PipeChannel final + { + public: + /** + * @brief Constructs an IPC pipe channel with close-on-exec (O_CLOEXEC) semantics. + * @param nonBlocking When true, sets O_NONBLOCK on both descriptor ends. + */ + explicit inline PipeChannel(bool nonBlocking = false) + { + auto descriptors = std::array{-1, -1}; + const auto flags = O_CLOEXEC | (nonBlocking ? O_NONBLOCK : 0); + throwErrno(::pipe2(std::ranges::data(descriptors), flags), "Failed to create IPC pipe channel"); + m_read = FileDescriptor{descriptors[0]}; + m_write = FileDescriptor{descriptors[1]}; + } + + PipeChannel(const PipeChannel&) = delete; + PipeChannel& operator=(const PipeChannel&) = delete; + PipeChannel(PipeChannel&&) noexcept = default; + PipeChannel& operator=(PipeChannel&&) noexcept = default; + + inline ~PipeChannel() = default; + + /// @brief Closes the read end of the pipe channel. + inline auto closeRead() -> void + { + m_read.close(); + } + + /// @brief Closes the write end of the pipe channel. + inline auto closeWrite() -> void + { + m_write.close(); + } + + /// @brief Returns the raw file descriptor for the read end, or -1 if closed. + [[nodiscard]] inline auto readDescriptor() const noexcept -> int + { + return m_read.get(); + } + + /// @brief Returns the raw file descriptor for the write end, or -1 if closed. + [[nodiscard]] inline auto writeDescriptor() const noexcept -> int + { + return m_write.get(); + } + + /** + * @brief Writes a contiguous byte span into the channel. + * @param bytes Data bytes to transmit. + * @return Total number of bytes written, or 0 if descriptor is closed. + */ + inline auto write(std::span bytes) -> std::size_t + { + if (m_write.get() < 0 or std::ranges::empty(bytes)) + return 0uz; + + auto totalWritten = 0uz; + for (; totalWritten < std::ranges::size(bytes);) + { + const auto result = ::write(m_write.get(), std::ranges::data(bytes) + totalWritten, std::ranges::size(bytes) - totalWritten); + if (result < 0) + { + if (errno == EINTR) + continue; + throwErrno(static_cast(result), "Failed to write to pipe channel"); + } + totalWritten += static_cast(result); + } + return totalWritten; + } + + /** + * @brief Writes a text string into the channel. + * @param text Character string view to transmit. + * @return Total number of bytes written. + */ + inline auto write(std::string_view text) -> std::size_t + { + return write(std::as_bytes(std::span{text})); + } + + /** + * @brief Writes a trivially copyable object directly into the channel. + * @param value Object reference to serialize as raw memory bytes. + */ + inline auto writeObject(const Type::TriviallyCopyable auto& value) -> void + { + const auto bytes = std::as_bytes(std::span{std::addressof(value), 1}); + write(bytes); + } + + /** + * @brief Reads up to buffer.size() bytes from the channel into the provided buffer. + * @param buffer Writable destination byte buffer. + * @return Number of bytes read (0 when the write end is closed or on end-of-stream). + */ + inline auto read(std::span buffer) -> std::size_t + { + if (m_read.get() < 0 or std::ranges::empty(buffer)) + return 0uz; + + while (true) + { + const auto result = ::read(m_read.get(), std::ranges::data(buffer), std::ranges::size(buffer)); + if (result < 0) + { + if (errno == EINTR) + continue; + throwErrno(static_cast(result), "Failed to read from pipe channel"); + } + return static_cast(result); + } + } + + /** + * @brief Reads a complete trivially copyable object from the channel. + * @param value Destination object reference populated from raw bytes. + * @return True if the complete object was read; false on premature EOF. + */ + inline auto readObject(Type::TriviallyCopyable auto& value) -> bool + { + auto buffer = std::as_writable_bytes(std::span{std::addressof(value), 1}); + for (auto totalRead = 0uz; totalRead < std::ranges::size(buffer);) + { + const auto bytes = read(buffer.subspan(totalRead)); + if (bytes == 0) + return false; + totalRead += bytes; + } + return true; + } + + /** + * @brief Reads all remaining bytes from the channel until end-of-file. + * @return Complete string containing all received bytes. + */ + [[nodiscard]] inline auto readAll() -> std::string + { + auto result = std::string{}; + auto buffer = std::array{}; + while (true) + { + const auto bytesRead = ::read(m_read.get(), std::ranges::data(buffer), std::ranges::size(buffer)); + if (bytesRead < 0) + { + if (errno == EINTR) + continue; + break; + } + if (bytesRead == 0) + break; + result.append(std::ranges::data(buffer), static_cast(bytesRead)); + } + return result; + } + + /** + * @brief Waits for incoming data or pipe hangup until the specified timeout expires. + * @param timeout Maximum duration to wait. + * @return True if data is available or hangup occurred; false on timeout or error. + */ + [[nodiscard]] inline auto waitForData(Chrono::Duration auto timeout) const -> bool + { + if (m_read.get() < 0) + return false; + + const auto deadline = std::chrono::steady_clock::now() + timeout; + auto pollDescriptor = pollfd{ + .fd = m_read.get(), + .events = POLLIN | POLLHUP | POLLERR, + .revents = 0}; + + while (true) + { + const auto now = std::chrono::steady_clock::now(); + if (now >= deadline) + return false; + + const auto remainingMilliseconds = std::chrono::duration_cast(deadline - now); + const auto timeoutMilliseconds = std::max(1L, static_cast(remainingMilliseconds.count())); + + const auto pollResult = ::poll(std::addressof(pollDescriptor), 1, static_cast(timeoutMilliseconds)); + if (pollResult > 0) + return (pollDescriptor.revents & (POLLIN | POLLHUP | POLLERR)) != 0; + if (pollResult < 0 and errno == EINTR) + continue; + return false; + } + } + + private: + FileDescriptor m_read; + FileDescriptor m_write; }; #endif } diff --git a/modules/System/Process.mpp b/modules/System/Process.mpp new file mode 100644 index 00000000..1c1e353f --- /dev/null +++ b/modules/System/Process.mpp @@ -0,0 +1,396 @@ +module; + +#include + +#include + +#if defined(OS_LINUX) or defined(OS_MACOS) +# include +# include +# include +#endif + +export module CppUtils.System.Process; + +import std; + +#if defined(OS_LINUX) or defined(OS_MACOS) +import CppUtils.System.Error; +#endif + +export namespace CppUtils::System +{ +#if defined(OS_LINUX) or defined(OS_MACOS) + using ProcessId = pid_t; +#endif + +#if defined(OS_LINUX) or defined(OS_MACOS) + /** + * @brief Strongly-typed enumeration representing process signals across POSIX and non-POSIX platforms. + * + * Under POSIX systems (Linux, macOS), enumerators map directly to system macros provided by ``. + * + * Under non-POSIX systems (such as Windows), the C Runtime only defines ISO C signals (`SIGABRT`, `SIGFPE`, + * `SIGILL`, `SIGINT`, `SIGSEGV`, `SIGTERM`). Standard POSIX values (IEEE Std 1003.1 / Linux x86-64 ABI) + * are provided as fallback constants to guarantee compile-time compatibility with cross-platform test + * runners, event watchers, and matchers without requiring platform-specific `#ifdef` guards at call sites. + * + * @see https://manpages.debian.org/signal.7 (Linux manual: signal(7)) + * @see https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html (POSIX.1-2017: signal.h) + * @see https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants (Microsoft Learn: Signal Constants) + */ + enum class Signal : int + { + Abort = SIGABRT, + Alarm = SIGALRM, + Bus = SIGBUS, + Child = SIGCHLD, + Continue = SIGCONT, +# ifdef SIGEMT + EmulatorTrap = SIGEMT, +# endif + FloatingPointException = SIGFPE, + Hangup = SIGHUP, + IllegalInstruction = SIGILL, + Interrupt = SIGINT, + Io = SIGIO, + Kill = SIGKILL, +# ifdef SIGLOST + Lost = SIGLOST, +# endif + Pipe = SIGPIPE, + ProfilingTimer = SIGPROF, +# ifdef SIGPWR + PowerFailure = SIGPWR, +# endif + Quit = SIGQUIT, + SegmentationFault = SIGSEGV, +# ifdef SIGSTKFLT + StackFault = SIGSTKFLT, +# endif + Stop = SIGSTOP, + System = SIGSYS, + Terminate = SIGTERM, + Trap = SIGTRAP, + TerminalStop = SIGTSTP, + TerminalInput = SIGTTIN, + TerminalOutput = SIGTTOU, + Urgent = SIGURG, + User1 = SIGUSR1, + User2 = SIGUSR2, + VirtualAlarm = SIGVTALRM, + WindowChange = SIGWINCH, + CpuTimeLimit = SIGXCPU, + FileSizeLimit = SIGXFSZ + }; +#else + /* + Fallback Signal enumeration for non-POSIX platforms (such as Windows). + Follows the official IEEE Std 1003.1 (POSIX.1) standard signal table (signals 1 to 31). + */ + enum class Signal : int + { + Abort = SIGABRT_COMPAT, // POSIX standard (6); avoids collision with TerminalOutput (22) because MSVC SIGABRT is 22 + Alarm = 14, + Bus = 7, + Child = 17, + Continue = 18, + FloatingPointException = SIGFPE, + Hangup = 1, + IllegalInstruction = SIGILL, + Interrupt = SIGINT, + Io = 29, + Kill = 9, + Pipe = 13, + ProfilingTimer = 27, + Quit = 3, + SegmentationFault = SIGSEGV, + Stop = 19, + System = 31, + Terminate = SIGTERM, + Trap = 5, + TerminalStop = 20, + TerminalInput = 21, + TerminalOutput = 22, // POSIX standard SIGTTOU (22) + Urgent = 23, + User1 = 10, + User2 = 12, + VirtualAlarm = 26, + WindowChange = 28, + CpuTimeLimit = 24, + FileSizeLimit = 25 + }; +#endif + + /** + * @brief Returns the symbolic name of a typed Signal. + * + * @param signal Strongly-typed signal enumerator. + * @return String view of the signal name (e.g. "SIGSEGV", "SIGKILL"), or "Unknown". + * + * @see https://manpages.debian.org/signal.7 (Linux manual: signal(7)) + * @see https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html (POSIX.1-2017: signal.h) + */ + [[nodiscard]] constexpr auto signalToString(Signal signal) noexcept -> std::string_view + { + switch (signal) + { + case Signal::Abort: return "SIGABRT"; + case Signal::Alarm: return "SIGALRM"; + case Signal::Bus: return "SIGBUS"; + case Signal::Child: return "SIGCHLD"; + case Signal::Continue: return "SIGCONT"; +#ifdef SIGEMT + case Signal::EmulatorTrap: return "SIGEMT"; +#endif + case Signal::FloatingPointException: return "SIGFPE"; + case Signal::Hangup: return "SIGHUP"; + case Signal::IllegalInstruction: return "SIGILL"; + case Signal::Interrupt: return "SIGINT"; + case Signal::Io: return "SIGIO"; + case Signal::Kill: return "SIGKILL"; +#ifdef SIGLOST + case Signal::Lost: return "SIGLOST"; +#endif + case Signal::Pipe: return "SIGPIPE"; + case Signal::ProfilingTimer: return "SIGPROF"; +#ifdef SIGPWR + case Signal::PowerFailure: return "SIGPWR"; +#endif + case Signal::Quit: return "SIGQUIT"; + case Signal::SegmentationFault: return "SIGSEGV"; +#ifdef SIGSTKFLT + case Signal::StackFault: return "SIGSTKFLT"; +#endif + case Signal::Stop: return "SIGSTOP"; + case Signal::System: return "SIGSYS"; + case Signal::Terminate: return "SIGTERM"; + case Signal::Trap: return "SIGTRAP"; + case Signal::TerminalStop: return "SIGTSTP"; + case Signal::TerminalInput: return "SIGTTIN"; + case Signal::TerminalOutput: return "SIGTTOU"; + case Signal::Urgent: return "SIGURG"; + case Signal::User1: return "SIGUSR1"; + case Signal::User2: return "SIGUSR2"; + case Signal::VirtualAlarm: return "SIGVTALRM"; + case Signal::WindowChange: return "SIGWINCH"; + case Signal::CpuTimeLimit: return "SIGXCPU"; + case Signal::FileSizeLimit: return "SIGXFSZ"; + default: return "Unknown"; + } + } + + /** + * @brief Returns a human-readable diagnostic explanation for a termination signal. + * @param signal Strongly-typed signal enumerator. + * @return Detailed explanation of the signal's probable cause, or empty string view if unknown. + * + * @see https://manpages.debian.org/signal.7 (Linux manual: signal(7)) + */ + [[nodiscard]] constexpr auto signalToExplanation(Signal signal) noexcept -> std::string_view + { + switch (signal) + { + case Signal::Abort: return "Abnormal termination (failed assertion, std::abort, unhandled exception)"; + case Signal::Alarm: return "Alarm clock (process watchdog or test timeout expired)"; + case Signal::Bus: return "Bus error (unaligned memory access, truncated mapped file)"; + case Signal::Child: return "Child status changed (stopped, continued, or exited)"; + case Signal::Continue: return "Continue execution if stopped"; +#ifdef SIGEMT + case Signal::EmulatorTrap: return "Emulator trap (hardware emulation trap)"; +#endif + case Signal::FloatingPointException: return "Arithmetic exception (integer division/modulo by zero, arithmetic overflow)"; + case Signal::Hangup: return "Hangup detected on controlling terminal or death of controlling process"; + case Signal::IllegalInstruction: return "Illegal instruction (corrupted instruction pointer, compiler trap)"; + case Signal::Interrupt: return "Terminal interrupt (interactive attention, Ctrl+C)"; + case Signal::Io: return "I/O possible (asynchronous I/O event)"; + case Signal::Kill: return "Killed (unconditional termination, OOM killer, kill -9)"; +#ifdef SIGLOST + case Signal::Lost: return "Resource or file lock lost"; +#endif + case Signal::Pipe: return "Broken pipe (attempt to write to a pipe or socket with no readers)"; + case Signal::ProfilingTimer: return "Profiling timer expired"; +#ifdef SIGPWR + case Signal::PowerFailure: return "Power failure restart"; +#endif + case Signal::Quit: return "Terminal quit (quit request from keyboard with core dump)"; + case Signal::SegmentationFault: return "Segmentation fault (invalid memory access, null pointer dereference)"; +#ifdef SIGSTKFLT + case Signal::StackFault: return "Stack fault on coprocessor"; +#endif + case Signal::Stop: return "Stop process execution (cannot be caught or ignored)"; + case Signal::System: return "Bad system call (invalid syscall number or seccomp filter violation)"; + case Signal::Terminate: return "Termination request (graceful termination signal, external kill)"; + case Signal::Trap: return "Trace/breakpoint trap"; + case Signal::TerminalStop: return "Terminal stop signal (interactive suspend, Ctrl+Z)"; + case Signal::TerminalInput: return "Terminal input for background process"; + case Signal::TerminalOutput: return "Terminal output for background process"; + case Signal::Urgent: return "Urgent or out-of-band data on socket"; + case Signal::User1: return "User-defined signal 1"; + case Signal::User2: return "User-defined signal 2"; + case Signal::VirtualAlarm: return "Virtual timer expired"; + case Signal::WindowChange: return "Window size change"; + case Signal::CpuTimeLimit: return "CPU time limit exceeded (resource limit exceeded)"; + case Signal::FileSizeLimit: return "File size limit exceeded"; + default: return ""; + } + } + + /** + * @brief Encapsulates the execution status of a child process. + * + * Wraps the POSIX wait status integer and provides friendly inspection methods + * for exit codes, abnormal termination signals, and detailed diagnostic causes. + */ +#if defined(OS_LINUX) or defined(OS_MACOS) + class ProcessStatus final + { + public: + /** + * @brief Constructs a ProcessStatus from a raw POSIX wait status integer. + * @param rawStatus Integer returned by waitpid / wait. + */ + constexpr explicit ProcessStatus(int rawStatus = 0) noexcept: + m_status{rawStatus} + {} + + /** + * @brief Returns the raw POSIX status code. + * @return Raw status integer. + */ + [[nodiscard]] constexpr auto raw() const noexcept -> int + { + return m_status; + } + + /** + * @brief Checks if the process terminated normally via exit() or returning from main(). + * @return True if the process exited normally. + */ + [[nodiscard]] constexpr auto isExited() const noexcept -> bool + { + return WIFEXITED(m_status); + } + + /** + * @brief Returns the exit code of the process if it terminated normally. + * @return Exit status integer (0-255). + */ + [[nodiscard]] constexpr auto getExitCode() const noexcept -> int + { + return WEXITSTATUS(m_status); + } + + /** + * @brief Checks if the process was terminated by an unhandled signal. + * @return True if the process was terminated by a signal. + */ + [[nodiscard]] constexpr auto isSignaled() const noexcept -> bool + { + return WIFSIGNALED(m_status); + } + + /** + * @brief Returns the signal that caused the process to terminate. + * @return Strongly-typed Signal enumerator. + */ + [[nodiscard]] constexpr auto getTerminalSignal() const noexcept -> std::optional + { + if (not isSignaled()) + return std::nullopt; +# if defined(_WIN32) + return std::nullopt; +# else + return static_cast(WTERMSIG(m_status)); +# endif + } + + /** + * @brief Checks if the process generated a core dump upon abnormal termination. + * @return True if a core dump was produced. + */ + [[nodiscard]] constexpr auto isCoreDumped() const noexcept -> bool + { +# if defined(WCOREDUMP) + return WCOREDUMP(m_status); +# else + return false; +# endif + } + + /** + * @brief Returns the symbolic name of the termination signal if signaled. + * @return Signal name (e.g. "SIGSEGV"), or empty string view if not signaled. + */ + [[nodiscard]] constexpr auto getTerminalSignalName() const noexcept -> std::string_view + { + using namespace std::literals; + + const auto signal = getTerminalSignal(); + return signal.has_value() ? signalToString(signal.value()) : ""sv; + } + + /** + * @brief Returns a diagnostic explanation of the termination signal if signaled. + * @return Signal explanation, or empty string view if not signaled or unknown. + */ + [[nodiscard]] constexpr auto getTerminalSignalExplanation() const noexcept -> std::string_view + { + using namespace std::literals; + + const auto signal = getTerminalSignal(); + return signal.has_value() ? signalToExplanation(signal.value()) : ""sv; + } + + /** + * @brief Formats a comprehensive diagnostic string describing the terminating signal. + * @return Formatted string such as "SIGSEGV (11) : Segmentation fault (...)", or empty string if not signaled. + */ + [[nodiscard]] inline auto getSignalDescription() const -> std::string + { + const auto signal = getTerminalSignal(); + if (not signal.has_value()) + return {}; + + const auto signalNumber = static_cast(signal.value()); + const auto signalName = signalToString(signal.value()); + const auto signalExplanation = signalToExplanation(signal.value()); + + if (std::ranges::empty(signalExplanation)) + return std::format("{} ({})", signalName, signalNumber); + + return std::format("{} ({}) : {}", signalName, signalNumber, signalExplanation); + } + + private: + int m_status = 0; + }; + + /// @brief Creates a new child process via fork(). + [[nodiscard]] inline auto fork() -> ProcessId + { + std::fflush(nullptr); + return ::fork(); + } + + /// @brief Sends a signal to a process using kill(). + inline auto kill(ProcessId processIdentifier, Signal signal = Signal::Kill) -> void + { + ::kill(processIdentifier, static_cast(signal)); + } + + /// @brief Waits for state changes in a child process via waitpid(). + inline auto wait(ProcessId processIdentifier, int options = 0) -> ProcessStatus + { + auto status = 0; + ::waitpid(processIdentifier, std::addressof(status), options); + return static_cast(status); + } + + /// @brief Terminates the calling process immediately without calling atexit or global destructors. + [[noreturn]] inline auto quickExit(int exitCode = 0) noexcept -> void + { + ::_exit(exitCode); + } +#endif +} diff --git a/modules/System/Sandbox.mpp b/modules/System/Sandbox.mpp new file mode 100644 index 00000000..8211cc74 --- /dev/null +++ b/modules/System/Sandbox.mpp @@ -0,0 +1,176 @@ +module; + +#include + +#if defined(OS_LINUX) +# include +#endif + +export module CppUtils.System.Sandbox; + +import std; + +#if defined(OS_LINUX) +import CppUtils.System.Pipe; +import CppUtils.System.Process; +#endif + +export namespace CppUtils::System +{ +#if defined(OS_LINUX) + /// @brief Execution outcome status recorded inside an isolated process. + enum class ExecutionStatus : std::uint8_t + { + Success = 0, + Failure = 1 + }; + + /// @brief Crash diagnostic details when a child process terminates abnormally. + struct ProcessCrashDetails final + { + Signal signal; + std::string diagnostic = {}; + bool coreDumped = false; + bool timedOut = false; + + [[nodiscard]] constexpr auto signalName() const noexcept -> std::string_view + { + return signalToString(signal); + } + + [[nodiscard]] constexpr auto signalExplanation() const noexcept -> std::string_view + { + return signalToExplanation(signal); + } + }; + + /// @brief Result payload and execution metrics returned from an isolated process run. + struct IsolatedExecutionResult final + { + ExecutionStatus status = ExecutionStatus::Success; + std::string payload = {}; + std::chrono::nanoseconds duration = {}; + }; + + /// @brief Failure details when an isolated child process execution encounters an error, timeout, or crash. + struct IsolatedExecutionFailure final + { + std::string reason = {}; + std::optional crash = std::nullopt; + }; + + /** + * @brief Executes a callable in an isolated child process, supervising execution and handling completion, timeout, and crash signals. + * @param function Routine to execute in the child process. + * @param timeout Maximum allowable execution duration (0ms for unlimited). + * @param onBeforeExecution Optional hook invoked inside the child process immediately after fork. + * @return Successful execution result or failure details. + */ + inline auto runIsolated( + std::invocable auto&& function, + std::chrono::milliseconds timeout = std::chrono::milliseconds{0}, + std::function onBeforeExecution = {}) + -> std::expected + { + using namespace std::chrono_literals; + + const auto startTime = std::chrono::steady_clock::now(); + auto channel = PipeChannel{}; + + const auto childProcessId = fork(); + if (childProcessId < 0) + return std::unexpected(IsolatedExecutionFailure{.reason = "fork() failed"}); + + if (childProcessId == 0) + { + channel.closeRead(); + if (onBeforeExecution) + onBeforeExecution(); + + try + { + function(); + channel.writeObject(ExecutionStatus::Success); + const auto messageLength = std::uint32_t{0}; + channel.writeObject(messageLength); + } + catch (const std::exception& exception) + { + channel.writeObject(ExecutionStatus::Failure); + const auto message = std::string_view{exception.what()}; + const auto length = static_cast(std::ranges::size(message)); + channel.writeObject(length); + if (length > 0) + channel.write(message); + } + catch (...) + { + channel.writeObject(ExecutionStatus::Failure); + const auto message = std::string_view{"Unknown exception occurred"}; + const auto length = static_cast(std::ranges::size(message)); + channel.writeObject(length); + channel.write(message); + } + + channel.closeWrite(); + quickExit(0); + } + + channel.closeWrite(); + + if (timeout > 0ms) + if (const auto hasData = channel.waitForData(timeout); not hasData) + { + kill(childProcessId, Signal::Kill); + auto _ = wait(childProcessId); + channel.closeRead(); + return std::unexpected(IsolatedExecutionFailure{ + .reason = std::format("Process execution timed out after {}ms", timeout.count()), + .crash = ProcessCrashDetails{ + .signal = Signal::Kill, + .diagnostic = std::format("Terminated by {} after timeout", signalToString(Signal::Kill)), + .coreDumped = false, + .timedOut = true}}); + } + + const auto processStatus = wait(childProcessId); + const auto duration = std::chrono::duration_cast(std::chrono::steady_clock::now() - startTime); + + if (processStatus.isSignaled()) + { + channel.closeRead(); + const auto signal = processStatus.getTerminalSignal().value(); + return std::unexpected(IsolatedExecutionFailure{ + .reason = std::format("Process terminated by signal {}", signalToString(signal)), + .crash = ProcessCrashDetails{ + .signal = signal, + .diagnostic = processStatus.getSignalDescription(), + .coreDumped = processStatus.isCoreDumped(), + .timedOut = false}}); + } + + auto status = ExecutionStatus::Failure; + auto messageLength = std::uint32_t{0}; + auto payload = std::string{}; + + if (channel.readObject(status) and channel.readObject(messageLength) and messageLength > 0) + { + payload.resize(messageLength); + for (auto totalRead = 0uz; totalRead < messageLength;) + { + const auto bytes = channel.read(std::as_writable_bytes(std::span{std::ranges::data(payload) + totalRead, messageLength - totalRead})); + if (bytes == 0) + break; + totalRead += bytes; + } + } + + channel.closeRead(); + + return IsolatedExecutionResult{ + .status = status, + .payload = std::move(payload), + .duration = duration}; + } +#endif +} diff --git a/modules/System/System.mpp b/modules/System/System.mpp index 082a4b4e..913e6e47 100644 --- a/modules/System/System.mpp +++ b/modules/System/System.mpp @@ -14,6 +14,8 @@ export import CppUtils.System.Error; export import CppUtils.System.FileDescriptor; export import CppUtils.System.Main; export import CppUtils.System.Pipe; +export import CppUtils.System.Process; +export import CppUtils.System.Sandbox; export import CppUtils.System.SharedLibrary; export import CppUtils.System.Type; diff --git a/modules/Terminal/Canvas.mpp b/modules/Terminal/Canvas.mpp index d7edebf4..2cf4f48c 100644 --- a/modules/Terminal/Canvas.mpp +++ b/modules/Terminal/Canvas.mpp @@ -138,8 +138,6 @@ export namespace CppUtils::Terminal }); flushBuffer(); - if (needFlush) - std::fflush(stdout); if (const auto endLine = startLine + viewportSize.height(); endLine < terminalSize.height()) @@ -149,8 +147,9 @@ export namespace CppUtils::Terminal const auto terminalHeight = getTerminalSize().height(); setCursorPosition({0, terminalHeight > 0 ? terminalHeight - 1 : 0}); std::fwrite("\n", sizeof(char), 1, stdout); - std::fflush(stdout); } + + std::fflush(stdout); } inline auto print() noexcept -> void diff --git a/modules/Terminal/TextModifier.mpp b/modules/Terminal/TextModifier.mpp index 9c50b9a9..fe98e602 100644 --- a/modules/Terminal/TextModifier.mpp +++ b/modules/Terminal/TextModifier.mpp @@ -65,7 +65,7 @@ export namespace CppUtils::Terminal } #if defined(OS_WINDOWS) - [[nodiscard]] inline static auto getTextColor(std::FILE* file) -> std::uint16_t + [[nodiscard]] static inline auto getTextColor(std::FILE* file) -> std::uint16_t { CONSOLE_SCREEN_BUFFER_INFO info; if (not GetConsoleScreenBufferInfo(getTerminalHandle(file), std::addressof(info))) diff --git a/modules/Thread/SharedLocker.mpp b/modules/Thread/SharedLocker.mpp index fbf57219..30f12f6d 100644 --- a/modules/Thread/SharedLocker.mpp +++ b/modules/Thread/SharedLocker.mpp @@ -172,7 +172,9 @@ export namespace CppUtils::Thread using UniqueAccessor = Accessor; using SharedAccessor = ReadOnlyAccessor; - inline constexpr SharedLocker() requires std::default_initializable = default; + inline constexpr SharedLocker() + requires std::default_initializable + = default; /** * @brief Constructs the protected resource in place. diff --git a/modules/Type/Concept.mpp b/modules/Type/Concept.mpp index 8f87ca9c..e590ade9 100644 --- a/modules/Type/Concept.mpp +++ b/modules/Type/Concept.mpp @@ -68,6 +68,22 @@ export namespace CppUtils::Type template concept NotCallable = not Callable; + template + concept Character = IsOneOf, char, wchar_t, char8_t, char16_t, char32_t>; + + template + concept ArithmeticInteger = std::integral> + and not std::same_as, bool> + and not Character; + template concept Numeric = std::integral or std::floating_point; + + /// @brief Resolves the decayed element type of a pointer, array, or scalar type. + template + using UnderlyingElementType = std::remove_cvref_t>>>; + + /// @brief Concept satisfied if a type is both a random-access range and a sized range. + template + concept SizedRandomAccessRange = std::ranges::random_access_range and std::ranges::sized_range; } diff --git a/modules/Type/Hash.mpp b/modules/Type/Hash.mpp index 57da4488..43900bbb 100644 --- a/modules/Type/Hash.mpp +++ b/modules/Type/Hash.mpp @@ -1,7 +1,7 @@ export module CppUtils.Type.Hash; import std; -import CppUtils.String.Concept; +import CppUtils.Type.Concept; namespace Detail { @@ -35,8 +35,8 @@ export namespace CppUtils::Type template [[nodiscard]] constexpr auto operator()(const T (&value)[N], Hash seed = HashOffset) const noexcept -> Hash { - if constexpr (String::Char) - return (*this)(std::basic_string_view{value}, seed); + if constexpr (Character) + return (*this)(std::basic_string_view{+value}, seed); else return (*this)(std::span{value}, seed); } diff --git a/modules/UnitTest/Assertion.mpp b/modules/UnitTest/Assertion.mpp new file mode 100644 index 00000000..5b675c62 --- /dev/null +++ b/modules/UnitTest/Assertion.mpp @@ -0,0 +1,155 @@ +export module CppUtils.UnitTest.Assertion; + +import std; +import CppUtils.Math.Utility; +import CppUtils.String; +import CppUtils.Type; + +namespace CppUtils::UnitTest +{ + /// @brief Relational comparison operators supported in assertion decomposition. + export enum class ComparisonOperator + { + Equal, + NotEqual, + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual + }; + + /// @brief Mapping between ComparisonOperator and its string representation. + export using ComparisonOperatorMapping = Type::Mapping< + Type::Pair, + Type::Pair, + Type::Pair"}>, + Type::Pair="}>>; + + /** + * @brief Converts a ComparisonOperator enumerator to its C++ operator string literal. + * @param comparisonOperator Enumerator value. + * @return Operator string view (e.g. `"=="`, `"<="`, `"!="`), or empty string if invalid. + */ + export [[nodiscard]] constexpr auto getComparisonOperatorString(ComparisonOperator comparisonOperator) noexcept -> std::string_view + { + using namespace std::literals; + return ComparisonOperatorMapping::toRhs(comparisonOperator, ""sv); + } + + /** + * @brief Parses an operator string literal into its corresponding ComparisonOperator. + * @param operatorString Operator symbol string (e.g. `"=="`, `"<="`). + * @return ComparisonOperator wrapped in std::optional, or std::nullopt if unknown. + */ + export [[nodiscard]] constexpr auto getComparisonOperator(std::string_view operatorString) noexcept -> std::optional + { + return ComparisonOperatorMapping::toLhs(operatorString); + } + + export template + constexpr auto applyOperator(const auto& left, const auto& right) -> bool + { + if constexpr (Operator == ComparisonOperator::Equal) + return (left == right); + else if constexpr (Operator == ComparisonOperator::NotEqual) + return (left != right); + else if constexpr (Operator == ComparisonOperator::LessThan) + return (left < right); + else if constexpr (Operator == ComparisonOperator::LessThanOrEqual) + return (left <= right); + else if constexpr (Operator == ComparisonOperator::GreaterThan) + return (left > right); + else if constexpr (Operator == ComparisonOperator::GreaterThanOrEqual) + return (left >= right); + } + + export template + constexpr auto compareFloatingPoint(const auto& left, const auto& right) -> bool + { + if constexpr (Operator == ComparisonOperator::Equal) + return Math::isEqual(left, right); + else if constexpr (Operator == ComparisonOperator::NotEqual) + return not Math::isEqual(left, right); + else if constexpr (Operator == ComparisonOperator::LessThanOrEqual) + return (left <= right) or Math::isEqual(left, right); + else if constexpr (Operator == ComparisonOperator::GreaterThanOrEqual) + return (left >= right) or Math::isEqual(left, right); + else + return applyOperator(left, right); + } + + export template + constexpr auto compareIntegers(const auto& left, const auto& right) -> bool + { + if constexpr (Operator == ComparisonOperator::Equal) + return std::cmp_equal(left, right); + else if constexpr (Operator == ComparisonOperator::NotEqual) + return std::cmp_not_equal(left, right); + else if constexpr (Operator == ComparisonOperator::LessThan) + return std::cmp_less(left, right); + else if constexpr (Operator == ComparisonOperator::LessThanOrEqual) + return std::cmp_less_equal(left, right); + else if constexpr (Operator == ComparisonOperator::GreaterThan) + return std::cmp_greater(left, right); + else if constexpr (Operator == ComparisonOperator::GreaterThanOrEqual) + return std::cmp_greater_equal(left, right); + } + + export template + constexpr auto evaluateComparison(const auto& left, const auto& right) -> bool + { + using CleanLeftType = std::remove_cvref_t; + using CleanRightType = std::remove_cvref_t; + + if constexpr (String::CharacterPointerOrArray and String::CharacterPointerOrArray) + { + using LeftCharacterType = String::CharacterType; + using RightCharacterType = String::CharacterType; + if constexpr (std::is_same_v) + return applyOperator(std::basic_string_view{left}, std::basic_string_view{right}); + else + return applyOperator(left, right); + } + else if constexpr (std::floating_point or std::floating_point) + { + using CommonFloatingPointType = std::conditional_t< + std::floating_point and std::floating_point, + std::common_type_t, + std::conditional_t, CleanLeftType, CleanRightType>>; + return compareFloatingPoint(static_cast(left), static_cast(right)); + } + else if constexpr (Type::ArithmeticInteger and Type::ArithmeticInteger) + { + return compareIntegers(left, right); + } + else if constexpr (std::ranges::input_range and std::ranges::input_range + and not String::GenericText and not String::GenericText + and not std::same_as and not std::same_as) + { + if constexpr (Operator == ComparisonOperator::Equal) + return std::ranges::equal(left, right, [](const auto& leftElement, const auto& rightElement) { + return evaluateComparison(leftElement, rightElement); + }); + else if constexpr (Operator == ComparisonOperator::NotEqual) + return not std::ranges::equal(left, right, [](const auto& leftElement, const auto& rightElement) { + return evaluateComparison(leftElement, rightElement); + }); + else + return applyOperator(left, right); + } + else if constexpr (std::same_as and std::integral) + { + return evaluateComparison(std::to_integer(left), right); + } + else if constexpr (std::integral and std::same_as) + { + return evaluateComparison(left, std::to_integer(right)); + } + else + { + return applyOperator(left, right); + } + } +} diff --git a/modules/UnitTest/Benchmark.mpp b/modules/UnitTest/Benchmark.mpp new file mode 100644 index 00000000..971bb55f --- /dev/null +++ b/modules/UnitTest/Benchmark.mpp @@ -0,0 +1,65 @@ +export module CppUtils.UnitTest.Benchmark; + +import std; +import CppUtils.Chrono; + +export namespace CppUtils::UnitTest +{ + /// @brief Execution measurement result of a benchmarked test block. + struct BenchmarkResult final + { + std::string name; ///< Name or label identifying the benchmarked operation. + std::chrono::nanoseconds duration{0}; ///< Measured execution duration in nanoseconds. + + /** + * @brief Formats the measured duration into a human-readable string (ns, us, ms, s). + * @return Formatted duration string. + */ + [[nodiscard]] auto durationString() const -> std::string + { + return Chrono::durationToString(duration); + } + }; + + /// @brief Comparison result between two benchmark measurements. + struct BenchmarkComparison final + { + BenchmarkResult first; ///< First benchmark measurement (baseline or candidate). + BenchmarkResult second; ///< Second benchmark measurement to compare against. + + /** + * @brief Computes the duration ratio of the first benchmark relative to the second. + * @return Duration ratio (first / second). + */ + [[nodiscard]] constexpr auto ratio() const -> double + { + const auto firstCount = static_cast(first.duration.count()); + const auto secondCount = static_cast(second.duration.count()); + if (secondCount == 0.0) + return 1.0; + return firstCount / secondCount; + } + + /** + * @brief Checks if the first benchmark executed faster than the second benchmark. + * @return True if first took less time than second. + */ + [[nodiscard]] constexpr auto isFirstFaster() const -> bool + { + return first.duration < second.duration; + } + + /** + * @brief Computes the speedup multiplier (> 1.0) of the faster benchmark over the slower one. + * @return Speedup factor. + */ + [[nodiscard]] constexpr auto speedup() const -> double + { + const auto firstCount = static_cast(first.duration.count()); + const auto secondCount = static_cast(second.duration.count()); + if (firstCount == 0.0 or secondCount == 0.0) + return 1.0; + return isFirstFaster() ? (secondCount / firstCount) : (firstCount / secondCount); + } + }; +} diff --git a/modules/UnitTest/EventWatcher.mpp b/modules/UnitTest/EventWatcher.mpp new file mode 100644 index 00000000..d0402002 --- /dev/null +++ b/modules/UnitTest/EventWatcher.mpp @@ -0,0 +1,201 @@ +export module CppUtils.UnitTest.EventWatcher; + +import std; +import CppUtils.Chrono.Concept; +import CppUtils.String.Hash; +import CppUtils.Type.Hash; +import CppUtils.Thread.UniqueLocker; + +export namespace CppUtils::UnitTest +{ + /** + * @brief Thread-safe RAII observer for monitoring emitted events and collecting payloads. + * + * Subscribes to the designated event upon construction and unsubscribes automatically upon destruction. + * Provides blocking synchronization utilities (waitForEvent, waitForOccurrences) + * and thread-safe inspection of occurrence counts and captured payloads. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#surveillance-dévénements-cpputilsunittesteventwatcher (Guide des tests unitaires) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#event-monitoring-cpputilsunittesteventwatcher (Unit test guide) + * + * @tparam eventName Compile-time hashed string identifying the event. + * @tparam Args Types of the event payload arguments. + */ + template + class EventWatcher final + { + public: + using Payload = std::conditional_t< + sizeof...(Args) == 0, + std::monostate, + std::conditional_t< + sizeof...(Args) == 1, + std::tuple_element_t<0, std::tuple>, + std::tuple>>; + + /** + * @brief Internal snapshot recording observed event occurrences and collected payloads. + * + * Synchronized via UniqueLocker and accessed through the occurrences() method. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#surveillance-dévénements-cpputilsunittesteventwatcher (Guide des tests unitaires) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#event-monitoring-cpputilsunittesteventwatcher (Unit test guide) + */ + struct State final + { + std::size_t count = 0uz; ///< Total number of times the monitored event has been fired. + std::vector payloads; ///< Sequential history of captured event payloads. + }; + + /** + * @brief Subscribes the watcher to the specified event emitter. + * @param emitter Reference to the event emitter to monitor. + */ + explicit EventWatcher(auto& emitter) + { + if constexpr (sizeof...(Args) == 0) + { + auto subscriptionIdentifier = emitter.template subscribe([this] { + { + auto accessor = m_state.access(); + ++accessor->count; + } + m_condition.notify_all(); + }); + m_unsubscriber = [&emitter, subscriptionIdentifier] { + emitter.unsubscribe(subscriptionIdentifier); + }; + } + else if constexpr (sizeof...(Args) == 1) + { + using FirstArg = std::tuple_element_t<0, std::tuple>; + auto subscriptionIdentifier = emitter.template subscribe([this](FirstArg value) { + { + auto accessor = m_state.access(); + ++accessor->count; + accessor->payloads.push_back(std::move(value)); + } + m_condition.notify_all(); + }); + m_unsubscriber = [&emitter, subscriptionIdentifier] { + emitter.unsubscribe(subscriptionIdentifier); + }; + } + else + { + auto subscriptionIdentifier = emitter.template subscribe([this](Args... args) { + { + auto accessor = m_state.access(); + ++accessor->count; + accessor->payloads.emplace_back(std::move(args)...); + } + m_condition.notify_all(); + }); + m_unsubscriber = [&emitter, subscriptionIdentifier] { + emitter.unsubscribe(subscriptionIdentifier); + }; + } + } + + inline ~EventWatcher() + { + if (m_unsubscriber) + m_unsubscriber(); + } + + EventWatcher(const EventWatcher&) = delete; + EventWatcher& operator=(const EventWatcher&) = delete; + + EventWatcher(EventWatcher&& other) noexcept: + m_unsubscriber{std::move(other.m_unsubscriber)}, + m_state{std::move(other.m_state)} + { + other.m_unsubscriber = nullptr; + } + + EventWatcher& operator=(EventWatcher&& other) noexcept + { + if (this != std::addressof(other)) + { + if (m_unsubscriber) + m_unsubscriber(); + m_unsubscriber = std::move(other.m_unsubscriber); + other.m_unsubscriber = nullptr; + m_state = std::move(other.m_state); + } + return *this; + } + + /** + * @brief Returns the total number of times the monitored event has been fired. + * @return Event occurrence count. + */ + [[nodiscard]] inline auto count() const noexcept -> std::size_t + { + return m_state.access()->count; + } + + /** + * @brief Checks if the monitored event has occurred at least once. + * @return True if count() > 0. + */ + [[nodiscard]] inline auto hasOccurred() const noexcept -> bool + { + return m_state.access()->count > 0uz; + } + + /** + * @brief Blocks until the event has been fired at least targetCount times or timeout expires. + * @tparam TimeoutDuration Duration type (defaults to std::chrono::milliseconds). + * @param targetCount Number of occurrences to wait for. + * @param timeout Maximum duration to wait before giving up. + * @return True if the target occurrence count was reached before the timeout expired. + */ + template + inline auto waitForOccurrences(std::size_t targetCount, TimeoutDuration timeout = std::chrono::milliseconds{1000}) -> bool + { + auto accessor = m_state.access(); + return m_condition.wait_for(accessor.getLockGuard(), timeout, [&] { + return accessor->count >= targetCount; + }); + } + + /** + * @brief Blocks until the event occurs at least once or timeout expires. + * @tparam TimeoutDuration Duration type (defaults to std::chrono::milliseconds). + * @param timeout Maximum duration to wait before giving up. + * @return True if the event occurred before the timeout expired. + */ + template + inline auto waitForEvent(TimeoutDuration timeout = std::chrono::milliseconds{1000}) -> bool + { + return waitForOccurrences(1uz, timeout); + } + + /** + * @brief Acquires exclusive locked access to the recorded state and payloads. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#surveillance-dévénements-cpputilsunittesteventwatcher (Guide des tests unitaires) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#event-monitoring-cpputilsunittesteventwatcher (Unit test guide) + * + * @return Unique accessor locking the internal state. + */ + [[nodiscard]] inline auto occurrences() const -> auto + { + return m_state.access(); + } + + /// @brief Resets the event occurrence count to zero and clears recorded payloads. + inline auto clear() -> void + { + auto accessor = m_state.access(); + accessor->count = 0uz; + accessor->payloads.clear(); + } + + private: + std::function m_unsubscriber; + mutable Thread::UniqueLocker m_state; + std::condition_variable m_condition; + }; +} diff --git a/modules/UnitTest/Expectation.mpp b/modules/UnitTest/Expectation.mpp new file mode 100644 index 00000000..ed7e60aa --- /dev/null +++ b/modules/UnitTest/Expectation.mpp @@ -0,0 +1,957 @@ +export module CppUtils.UnitTest.Expectation; + +import std; +import CppUtils.Chrono; +import CppUtils.Container; +import CppUtils.String; +import CppUtils.System; +import CppUtils.Terminal; +import CppUtils.Thread; +import CppUtils.Type; +import CppUtils.UnitTest.Assertion; +import CppUtils.UnitTest.Runner; + +namespace CppUtils::UnitTest +{ + /// @brief Base tag for all expectation matchers. + export struct ExpectationMatcherTag + {}; + + /** + * @brief Matcher for validating that a caught exception satisfies a predicate or value equality. + * @note Instantiated via throws; not intended to be constructed directly by user code. + */ + export template + struct ThrowsPredicateMatcher final: ExpectationMatcherTag + { + PredicateType predicate; + + constexpr explicit ThrowsPredicateMatcher(PredicateType predicateValue) noexcept: + predicate{std::move(predicateValue)} + {} + }; + + /** + * @brief Matcher for validating that a callable throws an expected exception type with optional message or predicate. + * @note Instantiated via throws; not intended to be constructed directly by user code. + */ + export template + struct ThrowsMatcher final: ExpectationMatcherTag + { + std::string_view expectedMessage{}; + + constexpr ThrowsMatcher() = default; + constexpr explicit ThrowsMatcher(std::string_view message) noexcept: + expectedMessage{message} + {} + + [[nodiscard]] constexpr auto operator()(std::string_view message) const noexcept -> ThrowsMatcher + { + return ThrowsMatcher{message}; + } + + template + requires (not std::convertible_to) + [[nodiscard]] constexpr auto operator()(PredicateOrValue&& predicateOrValue) const + { + return ThrowsPredicateMatcher>{std::forward(predicateOrValue)}; + } + }; + + /// @brief Matcher tag variable template for expecting an exception of type ExceptionType. + export template + inline constexpr ThrowsMatcher throws{}; + + /** + * @brief Matcher asserting that an action executes without throwing any exception. + * @note Instantiated via the 'noThrow' matcher; not intended to be constructed directly by user code. + */ + export struct NoThrowMatcher final: ExpectationMatcherTag + {}; + + /// @brief Matcher instance asserting that an action completes without throwing any exception. + export inline constexpr NoThrowMatcher noThrow{}; + + /** + * @brief Matcher asserting that executing an action terminates abnormally via a fatal signal. + * @note Instantiated via the 'crashes' matcher; not intended to be constructed directly by user code. + */ + export struct CrashesMatcher final: ExpectationMatcherTag + { + std::optional expectedSignal = std::nullopt; + + constexpr CrashesMatcher() = default; + constexpr explicit CrashesMatcher(System::Signal signal) noexcept: + expectedSignal{signal} + {} + + [[nodiscard]] constexpr auto operator()() const noexcept -> CrashesMatcher + { + return *this; + } + + [[nodiscard]] constexpr auto operator()(System::Signal signal) const noexcept -> CrashesMatcher + { + return CrashesMatcher{signal}; + } + }; + + /// @brief Matcher instance asserting that an action terminates abnormally. + export inline constexpr CrashesMatcher crashes{}; + + /** + * @brief Matcher asserting that a boolean predicate evaluates to true within an allotted duration. + * @note Instantiated via the 'eventually' matcher; not intended to be constructed directly by user code. + */ + export template + struct EventuallyMatcher final: ExpectationMatcherTag + { + TimeoutDuration timeout{std::chrono::ceil(std::chrono::milliseconds{1'000})}; + StepDuration step{std::chrono::ceil(std::chrono::milliseconds{20})}; + + constexpr EventuallyMatcher() = default; + constexpr explicit EventuallyMatcher( + TimeoutDuration timeoutDuration, + StepDuration stepDuration = std::chrono::ceil(std::chrono::milliseconds{20})) noexcept: + timeout{std::move(timeoutDuration)}, + step{std::move(stepDuration)} + {} + + [[nodiscard]] constexpr auto operator()(Chrono::Duration auto newTimeout) const noexcept + { + return EventuallyMatcher{ + std::move(newTimeout), + std::chrono::milliseconds{20}}; + } + + [[nodiscard]] constexpr auto operator()( + Chrono::Duration auto newTimeout, + Chrono::Duration auto newStep) const noexcept + { + return EventuallyMatcher{ + std::move(newTimeout), + std::move(newStep)}; + } + }; + + /// @brief Matcher instance asserting that a predicate eventually becomes true. + export inline constexpr EventuallyMatcher<> eventually{}; + + export template + struct CompletesWithMatcher; + + /** + * @brief Matcher asserting that an asynchronous future or reactive primitive completes within an allotted duration. + * @note Instantiated via the 'completes' matcher; not intended to be constructed directly by user code. + */ + export template + struct CompletesMatcher final: ExpectationMatcherTag + { + TimeoutDuration timeout{std::chrono::ceil(std::chrono::milliseconds{1'000})}; + + constexpr CompletesMatcher() = default; + constexpr explicit CompletesMatcher(TimeoutDuration timeoutDuration) noexcept: + timeout{std::move(timeoutDuration)} + {} + + [[nodiscard]] constexpr auto operator()(Chrono::Duration auto newTimeout) const noexcept + { + return CompletesMatcher{std::move(newTimeout)}; + } + + [[nodiscard]] constexpr auto with(auto&& expectedValue) const + { + return CompletesWithMatcher, TimeoutDuration>{ + std::forward(expectedValue), + timeout}; + } + }; + + /** + * @brief Matcher asserting that an asynchronous future completes with a specific value within an allotted duration. + * @note Instantiated via completes.with(...); not intended to be constructed directly by user code. + */ + export template + struct CompletesWithMatcher final: ExpectationMatcherTag + { + ExpectedValueType expectedValue; + TimeoutDuration timeout{std::chrono::ceil(std::chrono::milliseconds{1'000})}; + + constexpr CompletesWithMatcher( + ExpectedValueType value, + TimeoutDuration timeoutDuration = std::chrono::ceil(std::chrono::milliseconds{1'000})) noexcept: + expectedValue{std::move(value)}, + timeout{std::move(timeoutDuration)} + {} + + [[nodiscard]] constexpr auto operator()(Chrono::Duration auto newTimeout) const noexcept + { + return CompletesWithMatcher{expectedValue, std::move(newTimeout)}; + } + }; + + /// @brief Matcher instance asserting that a future or reactive primitive completes. + export inline constexpr CompletesMatcher<> completes{}; + + /// @brief Concept satisfied if Type is an expectation matcher (`throws`, `noThrow`, `crashes`, `eventually`, `completes`). + export template + concept ExpectationMatcherConcept = std::derived_from, ExpectationMatcherTag>; + + namespace Detail + { + [[nodiscard]] inline auto inspectConditionValue(const auto& value) -> std::optional + { + using DecayedType = std::remove_cvref_t; + + if constexpr (String::ExtensivelyPrintable) + return String::formatValue(value); + else + return std::nullopt; + } + + [[nodiscard]] inline auto formatColoredItem(const auto& item, std::string_view ansiColor = {}) -> std::string + { + using ItemType = std::remove_cvref_t; + const auto reset = std::ranges::empty(ansiColor) ? "" : Terminal::Ansi::EscapeCode::Reset; + + if constexpr (String::GenericText) + return std::format("\"{0}{1}{2}\"", ansiColor, item, reset); + else if constexpr (Type::Character) + return std::format("'{0}{1}{2}'", ansiColor, item, reset); + else + return std::format("{0}{1}{2}", ansiColor, String::formatValue(item), reset); + } + + [[nodiscard]] inline auto formatDiff(const auto& left, const auto& right) -> std::string + { + using DecayedLeft = std::remove_cvref_t; + using DecayedRight = std::remove_cvref_t; + + if constexpr (String::CharacterSequence and String::CharacterSequence) + { + const auto toView = [](const auto& target) -> std::string_view { + if constexpr (String::Text) + return std::string_view{target}; + else + { + const auto span = std::span{target}; + return std::string_view{std::ranges::data(span), std::ranges::size(span)}; + } + }; + + const auto leftView = toView(left); + const auto rightView = toView(right); + const auto differences = Container::Sequence::getDifferences(leftView, rightView); + + auto leftFormatted = std::string{}; + auto rightFormatted = std::string{}; + auto leftIndex = 0uz; + auto rightIndex = 0uz; + + auto leftInRed = false; + auto rightInGreen = false; + + for (const auto difference : differences) + { + switch (difference) + { + case Container::Sequence::Difference::None: + if (leftInRed) + { + leftFormatted += Terminal::Ansi::EscapeCode::Reset; + leftInRed = false; + } + if (rightInGreen) + { + rightFormatted += Terminal::Ansi::EscapeCode::Reset; + rightInGreen = false; + } + if (leftIndex < std::ranges::size(leftView)) + leftFormatted += leftView[leftIndex++]; + if (rightIndex < std::ranges::size(rightView)) + rightFormatted += rightView[rightIndex++]; + break; + + case Container::Sequence::Difference::Removed: + if (not leftInRed) + { + leftFormatted += Terminal::TextColor::Ansi::EscapeCode::Red; + leftInRed = true; + } + if (leftIndex < std::ranges::size(leftView)) + leftFormatted += leftView[leftIndex++]; + break; + + case Container::Sequence::Difference::Added: + if (not rightInGreen) + { + rightFormatted += Terminal::TextColor::Ansi::EscapeCode::Green; + rightInGreen = true; + } + if (rightIndex < std::ranges::size(rightView)) + rightFormatted += rightView[rightIndex++]; + break; + + case Container::Sequence::Difference::Edited: + if (not leftInRed) + { + leftFormatted += Terminal::TextColor::Ansi::EscapeCode::Red; + leftInRed = true; + } + if (not rightInGreen) + { + rightFormatted += Terminal::TextColor::Ansi::EscapeCode::Green; + rightInGreen = true; + } + if (leftIndex < std::ranges::size(leftView)) + leftFormatted += leftView[leftIndex++]; + if (rightIndex < std::ranges::size(rightView)) + rightFormatted += rightView[rightIndex++]; + break; + } + } + + if (leftInRed) + leftFormatted += Terminal::Ansi::EscapeCode::Reset; + if (rightInGreen) + rightFormatted += Terminal::Ansi::EscapeCode::Reset; + + return std::format("\n\nDiff:\n lhs (-): \"{}\"\n rhs (+): \"{}\"", leftFormatted, rightFormatted); + } + else if constexpr (std::same_as and std::same_as) + { + return formatDiff(left.string(), right.string()); + } + else if constexpr ((Type::Specializes or Type::Specializes) + and (Type::Specializes or Type::Specializes)) + { + if constexpr (std::tuple_size_v == std::tuple_size_v) + { + auto leftFormatted = std::string{"("}; + auto rightFormatted = std::string{"("}; + + [&](std::index_sequence) { + auto isFirst = true; + const auto processElement = [&]() { + if (not isFirst) + { + leftFormatted += ", "; + rightFormatted += ", "; + } + isFirst = false; + + const auto& leftElement = std::get(left); + const auto& rightElement = std::get(right); + + const auto areEqual = [&] { + if constexpr (requires { leftElement == rightElement; }) + return leftElement == rightElement; + else + return true; + }(); + + leftFormatted += formatColoredItem( + leftElement, + areEqual ? "" : Terminal::TextColor::Ansi::EscapeCode::Red); + rightFormatted += formatColoredItem( + rightElement, + areEqual ? "" : Terminal::TextColor::Ansi::EscapeCode::Green); + }; + + (processElement.template operator()(), ...); + }(std::make_index_sequence>{}); + + leftFormatted += ")"; + rightFormatted += ")"; + + return std::format("\n\nDiff:\n lhs (-): {}\n rhs (+): {}", leftFormatted, rightFormatted); + } + else + { + return {}; + } + } + else if constexpr (std::ranges::input_range and std::ranges::input_range + and not(String::GenericText or String::GenericText + or std::same_as or std::same_as)) + { + using LeftElement = std::ranges::range_value_t; + using RightElement = std::ranges::range_value_t; + + if constexpr (String::ExtensivelyPrintable and String::ExtensivelyPrintable) + { + auto leftVector = std::vector{}; + for (const auto& item : left) + leftVector.push_back(item); + + auto rightVector = std::vector{}; + for (const auto& item : right) + rightVector.push_back(item); + + if constexpr (requires { Container::Sequence::getDifferences(leftVector, rightVector); }) + { + const auto differences = Container::Sequence::getDifferences(leftVector, rightVector); + + auto leftFormatted = std::string{"["}; + auto rightFormatted = std::string{"["}; + auto leftIndex = 0uz; + auto rightIndex = 0uz; + auto leftFirst = true; + auto rightFirst = true; + + const auto appendLeft = [&](std::string_view ansiColor) { + if (leftIndex < std::ranges::size(leftVector)) + { + if (not leftFirst) + leftFormatted += ", "; + leftFirst = false; + leftFormatted += formatColoredItem(leftVector[leftIndex++], ansiColor); + } + }; + + const auto appendRight = [&](std::string_view ansiColor) { + if (rightIndex < std::ranges::size(rightVector)) + { + if (not rightFirst) + rightFormatted += ", "; + rightFirst = false; + rightFormatted += formatColoredItem(rightVector[rightIndex++], ansiColor); + } + }; + + for (const auto difference : differences) + { + if (difference != Container::Sequence::Difference::Added) + appendLeft(difference == Container::Sequence::Difference::None ? "" : Terminal::TextColor::Ansi::EscapeCode::Red); + if (difference != Container::Sequence::Difference::Removed) + appendRight(difference == Container::Sequence::Difference::None ? "" : Terminal::TextColor::Ansi::EscapeCode::Green); + } + + leftFormatted += "]"; + rightFormatted += "]"; + + return std::format("\n\nDiff:\n lhs (-): {}\n rhs (+): {}", leftFormatted, rightFormatted); + } + else + { + return {}; + } + } + else + { + return {}; + } + } + else + { + return {}; + } + } + } + + /** + * @brief Proxy object representing an expectation on a value or boolean condition. + * + * Enables comparison assertions such as `expect(a) == b;` as well as + * standalone boolean conditions such as `expect(condition);`. + * + * @tparam LeftType Type of the left-hand operand or condition. + */ + export template + class ExpectationProxy final + { + public: + constexpr explicit ExpectationProxy( + LeftType&& left, + std::source_location location = std::source_location::current()) noexcept: + m_left{std::forward(left)}, + m_location{location} + {} + + ExpectationProxy(const ExpectationProxy&) = delete; + ExpectationProxy& operator=(const ExpectationProxy&) = delete; + + constexpr ExpectationProxy(ExpectationProxy&& other) noexcept: + m_left{std::forward(other.m_left)}, + m_location{other.m_location}, + m_evaluated{std::exchange(other.m_evaluated, true)} + {} + + constexpr ~ExpectationProxy() noexcept(false) + { + if (not m_evaluated and std::uncaught_exceptions() == 0) + { + if constexpr (requires { static_cast(m_left); }) + { + if (not static_cast(m_left)) + failCondition("expect(condition)"); + } + else + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(value): value is not boolean and was not compared to anything.", + m_location.file_name(), + m_location.line(), + m_location.column())}; + } + } + } + + constexpr auto operator!() -> void + { + m_evaluated = true; + if constexpr (requires { static_cast(m_left); }) + { + if (static_cast(m_left)) + failCondition("not expect(condition)"); + } + else + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn not expect(value): value is not boolean and cannot be converted to bool.", + m_location.file_name(), + m_location.line(), + m_location.column())}; + } + } + + template + requires (not ExpectationMatcherConcept) + constexpr auto operator==(const RightType& right) -> void + { + check(right); + } + + template + requires (not ExpectationMatcherConcept) + constexpr auto operator!=(const RightType& right) -> void + { + check(right); + } + + template + requires (not ExpectationMatcherConcept) + constexpr auto operator<(const RightType& right) -> void + { + check(right); + } + + template + requires (not ExpectationMatcherConcept) + constexpr auto operator<=(const RightType& right) -> void + { + check(right); + } + + template + requires (not ExpectationMatcherConcept) + constexpr auto operator>(const RightType& right) -> void + { + check(right); + } + + template + requires (not ExpectationMatcherConcept) + constexpr auto operator>=(const RightType& right) -> void + { + check(right); + } + + template + inline auto operator==(const ThrowsMatcher& matcher) -> void + requires requires (LeftType& left) { left(); } + { + executeAndExpectException([&](const ExceptionType& exception) { + if (not std::ranges::empty(matcher.expectedMessage)) + { + if constexpr (requires (const ExceptionType& target) { target.what(); }) + { + if (const auto actualMessage = std::string_view{exception.what()}; + not actualMessage.contains(matcher.expectedMessage)) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == throws<{}>\nCaught expected exception of type {}, but message did not contain expected text.\nExpected: \"{}\"\nActual: \"{}\"", + m_location.file_name(), + m_location.line(), + m_location.column(), + typeid(ExceptionType).name(), + typeid(ExceptionType).name(), + matcher.expectedMessage, + actualMessage)}; + } + } + else if constexpr (requires { exception == matcher.expectedMessage; }) + { + if (not(exception == matcher.expectedMessage)) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == throws<{}>\nCaught expected exception of type {}, but value did not match expected value.", + m_location.file_name(), + m_location.line(), + m_location.column(), + typeid(ExceptionType).name(), + typeid(ExceptionType).name())}; + } + } + } + }); + } + + template + inline auto operator==(const ThrowsPredicateMatcher& matcher) -> void + requires requires (LeftType& left) { left(); } + { + executeAndExpectException([&](const ExceptionType& exception) { + if constexpr (std::is_invocable_r_v) + { + if (not matcher.predicate(exception)) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == throws<{}>\nCaught expected exception of type {}, but it failed predicate validation.", + m_location.file_name(), + m_location.line(), + m_location.column(), + typeid(ExceptionType).name(), + typeid(ExceptionType).name())}; + } + } + else if constexpr (std::is_invocable_v) + { + matcher.predicate(exception); + } + else if constexpr (requires { exception == matcher.predicate; }) + { + if (not(exception == matcher.predicate)) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == throws<{}>\nCaught expected exception of type {}, but its value did not match expected value.", + m_location.file_name(), + m_location.line(), + m_location.column(), + typeid(ExceptionType).name(), + typeid(ExceptionType).name())}; + } + } + }); + } + + inline auto operator==(const NoThrowMatcher&) -> void + requires requires (LeftType& left) { left(); } + { + m_evaluated = true; + try + { + m_left(); + } + catch (const std::exception& exception) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == noThrow\nExpected no throw, but an exception was thrown (type: {}): {}", + m_location.file_name(), + m_location.line(), + m_location.column(), + typeid(exception).name(), + exception.what())}; + } + catch (...) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == noThrow\nExpected no throw, but an unknown non-std exception was thrown.", + m_location.file_name(), + m_location.line(), + m_location.column())}; + } + } + + inline auto operator==([[maybe_unused]] const CrashesMatcher& matcher) -> void + requires requires (LeftType& left) { left(); } + { + m_evaluated = true; +#if defined(__linux__) + const auto isolatedResult = System::runIsolated([&] { + try + { + m_left(); + } + catch (...) + { + std::abort(); + } + }); + + if (not isolatedResult and isolatedResult.error().crash.has_value()) + { + const auto& crash = isolatedResult.error().crash.value(); + const auto actualSignal = crash.signal; + if (matcher.expectedSignal.has_value() and actualSignal != matcher.expectedSignal.value()) + { + const auto expectedSignalName = System::signalToString(matcher.expectedSignal.value()); + const auto actualSignalName = crash.signalName(); + auto message = std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == crashes\nExpected death with signal {} ({}), but terminated with signal {} ({})", + m_location.file_name(), + m_location.line(), + m_location.column(), + static_cast(matcher.expectedSignal.value()), + expectedSignalName, + static_cast(actualSignal), + actualSignalName); + if (const auto actualSignalExplanation = crash.signalExplanation(); + not std::ranges::empty(actualSignalExplanation)) + message += std::format("\nCause: {}", actualSignalExplanation); + throw TestException{std::move(message)}; + } + } + else + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == crashes\nExpected process to crash, but it exited normally", + m_location.file_name(), + m_location.line(), + m_location.column())}; + } +#else + throw TestSkipped{"crashes matcher is only supported on Linux with process isolation", m_location}; +#endif + } + + template + inline auto operator==(const EventuallyMatcher& matcher) -> void + requires requires (LeftType& left) { { left() } -> std::convertible_to; } + { + m_evaluated = true; + if (not Thread::pollFor([&] { return static_cast(m_left()); }, matcher.timeout, matcher.step)) [[unlikely]] + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(predicate) == eventually\nTimed out waiting for predicate to become true (waited for {})", + m_location.file_name(), + m_location.line(), + m_location.column(), + matcher.timeout)}; + } + + template + inline auto operator==(const CompletesMatcher& matcher) -> void + requires (requires (LeftType& left, TimeoutDuration timeout) { + { left.wait_for(timeout) } -> std::same_as; + } or requires (LeftType& left, TimeoutDuration timeout) { + { left().wait_for(timeout) } -> std::same_as; + }) + { + m_evaluated = true; + auto&& future = resolveFuture(); + waitForFuture(future, matcher.timeout, "completes"); + } + + template + inline auto operator==(const CompletesMatcher& matcher) -> void + requires requires (LeftType& left, TimeoutDuration timeout) { + { left.try_acquire_for(timeout) } -> std::same_as; + } + { + m_evaluated = true; + if (not m_left.try_acquire_for(matcher.timeout)) [[unlikely]] + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(semaphore) == completes\nTimed out waiting for semaphore to be acquired (waited for {})", + m_location.file_name(), + m_location.line(), + m_location.column(), + matcher.timeout)}; + } + } + + template + inline auto operator==(const CompletesWithMatcher& matcher) -> void + requires (requires (LeftType& left, TimeoutDuration timeout) { + { left.wait_for(timeout) } -> std::same_as; + left.get(); + } or requires (LeftType& left, TimeoutDuration timeout) { + { left().wait_for(timeout) } -> std::same_as; + left().get(); + }) + { + m_evaluated = true; + auto&& future = resolveFuture(); + waitForFuture(future, matcher.timeout, "completes.with(...)"); + checkFutureValue(future.get(), matcher.expectedValue); + } + + private: + [[nodiscard]] constexpr auto resolveFuture() -> decltype(auto) + { + if constexpr (requires { m_left(); }) + return m_left(); + else + return (m_left); + } + + template + auto waitForFuture(FutureType& future, TimeoutDuration timeout, std::string_view matcherName) const -> void + { + const auto status = future.wait_for(timeout); + if (status != std::future_status::ready) [[unlikely]] + { + if (status == std::future_status::timeout) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(future) == {}\nTimed out waiting for future to complete (waited for {})", + m_location.file_name(), + m_location.line(), + m_location.column(), + matcherName, + timeout)}; + } + else + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(future) == {}\nFuture execution was deferred", + m_location.file_name(), + m_location.line(), + m_location.column(), + matcherName)}; + } + } + } + + template + auto checkFutureValue(ActualValueType&& actualValue, const ExpectedValueType& expectedValue) const -> void + { + if (not(actualValue == expectedValue)) [[unlikely]] + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(future) == completes.with(...)\nFuture completed with unexpected value:\nExpected: {}\nActual: {}", + m_location.file_name(), + m_location.line(), + m_location.column(), + Detail::formatColoredItem(expectedValue, Terminal::TextColor::Ansi::EscapeCode::Green), + Detail::formatColoredItem(actualValue, Terminal::TextColor::Ansi::EscapeCode::Red))}; + } + } + + template + auto executeAndExpectException(auto&& validateException) -> void + { + m_evaluated = true; + try + { + m_left(); + } + catch (const ExceptionType& exception) + { + validateException(exception); + return; + } + catch (const std::exception& exception) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == throws<{}>\nExpected exception of type {}, but caught different exception (type: {}): {}", + m_location.file_name(), + m_location.line(), + m_location.column(), + typeid(ExceptionType).name(), + typeid(ExceptionType).name(), + typeid(exception).name(), + exception.what())}; + } + catch (...) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == throws<{}>\nExpected exception of type {}, but caught an unknown non-std exception.", + m_location.file_name(), + m_location.line(), + m_location.column(), + typeid(ExceptionType).name(), + typeid(ExceptionType).name())}; + } + + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(action) == throws<{}>\nExpected exception of type {}, but no exception was thrown.", + m_location.file_name(), + m_location.line(), + m_location.column(), + typeid(ExceptionType).name(), + typeid(ExceptionType).name())}; + } + + auto failCondition(std::string_view expressionName) const -> void + { + const auto diagnosticValue = Detail::inspectConditionValue(m_left); + if (diagnosticValue.has_value()) + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn {}\nwith value:\n{}{}{}", + m_location.file_name(), + m_location.line(), + m_location.column(), + expressionName, + Terminal::TextColor::Ansi::EscapeCode::Red, + *diagnosticValue, + Terminal::Ansi::EscapeCode::Reset)}; + } + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn {}", + m_location.file_name(), + m_location.line(), + m_location.column(), + expressionName)}; + } + + template + constexpr auto check(const auto& right) -> void + { + m_evaluated = true; + const auto passed = evaluateComparison(m_left, right); + if (not passed) [[unlikely]] + { + auto diffString = std::string{}; + if constexpr (Operator == ComparisonOperator::Equal) + diffString = Detail::formatDiff(m_left, right); + + throw TestException{std::format( + "In {}\nAt line {}, column {}\nIn expect(lhs) {} rhs\nwith lhs:\n{}\nand rhs:\n{}{}", + m_location.file_name(), + m_location.line(), + m_location.column(), + getComparisonOperatorString(Operator), + String::formatValue(m_left), + String::formatValue(right), + diffString)}; + } + } + + LeftType m_left; + std::source_location m_location; + bool m_evaluated = false; + }; + + /** + * @brief Asserts an expectation on a value, condition, or callable. + * + * Supports comparison assertions using relational operators: + * ```cpp + * expect(value) == expected; + * expect(score) >= 100; + * ``` + * standalone boolean conditions: + * ```cpp + * expect(result.has_value()); + * ``` + * and matchers: + * ```cpp + * expect([&] { divide(1, 0); }) == throws; + * expect([&] { divide(1, 2); }) == noThrow; + * ``` + * + * @param operand Value to compare, boolean condition, or callable to evaluate. + * @param location Source location used for failure reporting (automatically captured). + * @return ExpectationProxy evaluating the expectation. + */ + export template + constexpr auto expect( + OperandType&& operand, + std::source_location location = std::source_location::current()) noexcept + -> ExpectationProxy + { + return ExpectationProxy{std::forward(operand), location}; + } +} diff --git a/modules/UnitTest/JUnitFormatter.mpp b/modules/UnitTest/JUnitFormatter.mpp new file mode 100644 index 00000000..5c2aa145 --- /dev/null +++ b/modules/UnitTest/JUnitFormatter.mpp @@ -0,0 +1,147 @@ +export module CppUtils.UnitTest.JUnitFormatter; + +import std; +import CppUtils.Chrono; +import CppUtils.FileSystem; +import CppUtils.Language.Xml.Utility; +import CppUtils.Logger; +import CppUtils.Log.Config.UnitTests; +import CppUtils.UnitTest.Report; + +export namespace CppUtils::UnitTest::JUnit +{ + /** + * @brief Generates a standard JUnit XML document string from structured test run data. + * @param suites Span of recorded test suites. + * @param runName Test run suite name attribute (defaults to "CppUtils Unit Tests"). + * @param totalDuration Total wall-clock duration of the test run. + * @return Formatted XML document. + */ + [[nodiscard]] inline auto generateXml( + std::span suites, + std::string_view runName = "CppUtils Unit Tests", + std::chrono::nanoseconds totalDuration = {}) -> std::string + { + using namespace std::literals; + + auto formatSeconds = [](std::chrono::nanoseconds duration) -> std::string { + const auto seconds = static_cast(duration.count()) / 1'000'000'000.0; + return std::format("{:.4f}", seconds); + }; + + auto totalTests = 0uz; + auto totalFailures = 0uz; + auto totalSkipped = 0uz; + for (const auto& suite : suites) + { + totalTests += std::ranges::size(suite.testCases); + for (const auto& testCase : suite.testCases) + { + if (testCase.status == TestStatus::Failure) + ++totalFailures; + else if (testCase.status == TestStatus::Skipped) + ++totalSkipped; + } + } + + auto xml = R"()"s; + xml += std::format( + "\n" + R"()" + "\n", + Language::Xml::escapeXml(runName), + totalTests, + totalFailures, + totalSkipped, + formatSeconds(totalDuration)); + + for (const auto& suite : suites) + { + auto suiteFailures = 0uz; + auto suiteSkipped = 0uz; + for (const auto& testCase : suite.testCases) + { + if (testCase.status == TestStatus::Failure) + ++suiteFailures; + else if (testCase.status == TestStatus::Skipped) + ++suiteSkipped; + } + + xml += std::format( + R"( )" + "\n", + Language::Xml::escapeXml(suite.name), + std::ranges::size(suite.testCases), + suiteFailures, + suiteSkipped, + formatSeconds(suite.duration)); + + for (const auto& testCase : suite.testCases) + { + xml += std::format( + R"( )" + "\n", + Language::Xml::escapeXml(testCase.message)); + xml += " \n"; + } + else if (testCase.status == TestStatus::Failure) + { + xml += ">\n"; + const auto firstNewline = testCase.message.find('\n'); + const auto summaryMessage = (firstNewline != std::string::npos) ? testCase.message.substr(0, firstNewline) : testCase.message; + xml += std::format( + R"( {})" + "\n", + Language::Xml::escapeXml(summaryMessage), + Language::Xml::escapeXml(testCase.failureType), + Language::Xml::escapeXml(testCase.message)); + xml += " \n"; + } + } + + xml += " \n"; + } + + xml += "\n"; + return xml; + } + + /** + * @brief Serializes test execution results to a standard JUnit XML report file. + * @param filePath Output file destination path. + * @param suites Span of recorded test suites with their test cases and execution metrics. + * @param totalDuration Total wall-clock duration of the test run. + */ + inline auto writeJUnitXmlReport( + const std::filesystem::path& filePath, + std::span suites, + std::chrono::nanoseconds totalDuration = {}) -> void + { + try + { + if (const auto parentPath = filePath.parent_path(); not std::ranges::empty(parentPath)) + std::filesystem::create_directories(parentPath); + + const auto xml = generateXml(suites, "CppUtils Unit Tests", totalDuration); + FileSystem::String::write(filePath, xml); + Logger<"UnitTests">::print<"detail">("JUnit XML report generated: {}", filePath.string()); + } + catch (const std::exception& exception) + { + Logger<"UnitTests">::print<"error">("Failed to write JUnit XML report to {}: {}", filePath.string(), exception.what()); + } + } +} diff --git a/modules/UnitTest/LifetimeChecker.mpp b/modules/UnitTest/LifetimeChecker.mpp index c2245969..82e2e949 100644 --- a/modules/UnitTest/LifetimeChecker.mpp +++ b/modules/UnitTest/LifetimeChecker.mpp @@ -4,9 +4,24 @@ import std; export namespace CppUtils::UnitTest { + /** + * @brief Diagnostic utility for tracking and validating object lifecycles in unit tests. + * + * Monitors default construction, copy, move, and destruction events via a callback logger, + * while maintaining thread-safe atomic counters for copy and move operations. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#suivi-du-cycle-de-vie-des-objets--cpputilsunittestlifetimechecker (Guide des tests unitaires) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#tracking-object-lifecycle-cpputilsunittestlifetimechecker (Unit test guide) + */ class LifetimeChecker { public: + /** + * @brief Constructs a LifetimeChecker and logs object creation. + * @param callback Logging callback receiving string views of lifecycle events. + * @param name Diagnostic identifier of the monitored object. + * @param indentationLevel Tab indentation level for hierarchical visual logging. + */ inline LifetimeChecker(std::function callback, std::string name, std::size_t indentationLevel = 0): m_callback{std::move(callback)}, m_name{std::move(name)}, @@ -15,16 +30,25 @@ export namespace CppUtils::UnitTest m_callback(std::format("{}{}()", std::string(m_indentationLevel, '\t'), m_name)); } + /** + * @brief Returns the total number of copy constructions and copy assignments performed. + * @return Cumulative copy operation count. + */ [[nodiscard]] inline auto getCopyCount() const -> std::size_t { return m_copyCount; } + /** + * @brief Returns the total number of move constructions and move assignments performed. + * @return Cumulative move operation count. + */ [[nodiscard]] inline auto getMoveCount() const -> std::size_t { return m_moveCount; } + /// @brief Destroys the LifetimeChecker and logs the destruction event. virtual ~LifetimeChecker() { m_callback(std::format("{}~{}()", std::string(m_indentationLevel, '\t'), m_name)); diff --git a/modules/UnitTest/Report.mpp b/modules/UnitTest/Report.mpp new file mode 100644 index 00000000..53ba8133 --- /dev/null +++ b/modules/UnitTest/Report.mpp @@ -0,0 +1,33 @@ +export module CppUtils.UnitTest.Report; + +import std; + +export namespace CppUtils::UnitTest +{ + /// @brief Outcome status of an individual test case execution. + enum class TestStatus + { + Success, ///< Test executed successfully without errors or unmet expectations. + Failure, ///< Test failed due to an unmet assertion or unhandled exception. + Skipped ///< Test was explicitly skipped or bypassed. + }; + + /// @brief Recorded execution summary of an individual test case for reporting purposes. + struct RecordedTestCase final + { + std::string name = {}; ///< Name of the test case. + std::string className = {}; ///< Test suite or category name. + TestStatus status = TestStatus::Success; ///< Execution outcome. + std::string message = {}; ///< Failure diagnostic or skip explanation. + std::string failureType = "TestException"; ///< Failure exception type. + std::chrono::nanoseconds duration = {}; ///< Measured execution duration. + }; + + /// @brief Recorded execution summary of an entire test suite for reporting purposes. + struct RecordedTestSuite final + { + std::string name = {}; ///< Test suite identifier. + std::vector testCases = {}; ///< Recorded test cases within this suite. + std::chrono::nanoseconds duration = {}; ///< Total suite execution duration. + }; +} diff --git a/modules/UnitTest/Runner.mpp b/modules/UnitTest/Runner.mpp new file mode 100644 index 00000000..4942c28a --- /dev/null +++ b/modules/UnitTest/Runner.mpp @@ -0,0 +1,597 @@ +export module CppUtils.UnitTest.Runner; + +import std; +import CppUtils.Chrono; +import CppUtils.Container; +import CppUtils.Logger; +import CppUtils.Log.Config.UnitTests; +import CppUtils.System; +import CppUtils.Terminal; +import CppUtils.UnitTest.Report; +import CppUtils.UnitTest.JUnitFormatter; + +export namespace CppUtils::UnitTest +{ + // Workaround on Windows (MSVC ABI) + // https://github.com/llvm/llvm-project/issues/221609 +#if defined(_WIN32) + /** + * @brief Exception thrown upon test assertion failure. + * @note Raised internally by assertions (such as TestSuite::expect()) and caught by the test runner. Not intended to be instantiated or caught directly by users. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#écrire-des-assertions (Écrire des assertions) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#writing-assertions (Writing assertions) + */ + class TestException final: public std::exception + { + public: + /** + * @brief Constructs a TestException with a diagnostic failure message. + * @param message Diagnostic failure description. + */ + explicit inline TestException(std::string message): + m_message{std::move(message)} + {} + + [[nodiscard]] inline auto what() const noexcept -> const char* final + { + return m_message.c_str(); + } + + private: + std::string m_message; + }; + + /** + * @brief Exception thrown when a test execution is skipped via skip() or skipIf(). + * @note Raised internally by TestSuite::skip() and TestSuite::skipIf() to signal test skipping to the test runner. Not intended to be instantiated or caught directly by users. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#ignorer-délibérément-un-test--skip-et-skipif (Ignorer un test) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#deliberately-skipping-a-test-skip-and-skipif (Skipping a test) + */ + class TestSkipped final: public std::exception + { + public: + /** + * @brief Constructs a TestSkipped exception with justification and location. + * @param message Justification for skipping the test. + * @param location Source location used for skip reporting (automatically captured). + */ + explicit inline TestSkipped(std::string message, std::source_location location = std::source_location::current()): + m_message{std::format("{} (at {}:{})", message, location.file_name(), location.line())} + {} + + [[nodiscard]] inline auto what() const noexcept -> const char* final + { + return m_message.c_str(); + } + + private: + std::string m_message; + }; +#else + /** + * @brief Exception thrown upon test assertion failure. + * @note Raised internally by assertions (such as TestSuite::expect()) and caught by the test runner. Not intended to be instantiated or caught directly by users. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#écrire-des-assertions (Écrire des assertions) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#writing-assertions (Writing assertions) + */ + class TestException final: public std::runtime_error + { + public: + /** + * @brief Constructs a TestException with a diagnostic failure message. + * @param message Diagnostic failure description. + */ + explicit inline TestException(const std::string& message): + std::runtime_error{message} + {} + }; + + /** + * @brief Exception thrown when a test execution is skipped via skip() or skipIf(). + * @note Raised internally by TestSuite::skip() and TestSuite::skipIf() to signal test skipping to the test runner. Not intended to be instantiated or caught directly by users. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#ignorer-délibérément-un-test--skip-et-skipif (Ignorer un test) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#deliberately-skipping-a-test-skip-and-skipif (Skipping a test) + */ + class TestSkipped final: public std::runtime_error + { + public: + /** + * @brief Constructs a TestSkipped exception with justification and location. + * @param message Justification for skipping the test. + * @param location Source location used for skip reporting (automatically captured). + */ + explicit inline TestSkipped(const std::string& message, std::source_location location = std::source_location::current()): + std::runtime_error{std::format("{} (at {}:{})", message, location.file_name(), location.line())} + {} + }; +#endif + + /** + * @brief Configuration parameters controlling test discovery, filtering, and execution reporting. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#options-de-configuration-cpputilsunittesttestsettings (Guide des tests unitaires) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#configuration-options-cpputilsunittesttestsettings (Unit test guide) + */ + struct TestSettings final + { + bool verbose = true; ///< Enables standard test execution logging. + bool detail = true; ///< Enables detailed execution progress logging and formatting (separators, durations, comparisons). + bool chrono = false; ///< Displays execution durations for individual tests. + bool failFast = false; ///< Immediately stops execution upon the first failing test. + std::string filter = {}; ///< Substring filter for matching test suite names. + std::string report = {}; ///< Destination path or format:path for test report export (e.g. junit:build/report.xml). + bool abort = false; ///< Skips test execution (e.g. after displaying --help). + }; + + /** + * @brief Internal representation of an individual executable test case within a suite. + * @note Managed internally by TestSuite and TestRunner. Not intended to be instantiated directly by users. + */ + struct TestCase final + { + std::string name; ///< Descriptive name identifying this test case. + std::function function; ///< Callable test routine invoked during execution with a cooperative stop token. + std::chrono::seconds timeout = std::chrono::seconds{10}; ///< Maximum allowable execution duration before the watchdog timer terminates the test. + }; + + /** + * @brief Record of a test case intentionally skipped during suite registration or execution. + * @note Recorded internally by TestSuite and TestRunner when tests are skipped. Not intended to be instantiated directly by users. + */ + struct SkippedTest final + { + std::string name; ///< Name of the skipped test case. + std::string message; ///< Explanatory reason or requirement justifying why the test was skipped. + }; + + /** + * @brief Central test suite registry and execution engine. + * @note Managed internally by the test framework. Not intended to be instantiated directly by users. + */ + class TestRunner final + { + using Logger = Logger<"UnitTests">; + + public: + /** + * @brief Registers a test suite with the given name and test cases. + * @param name Unique name of the test suite. + * @param tests Collection of test cases belonging to this suite. + */ + inline auto addTestSuite(const std::string& name, std::vector tests) -> void + { + m_testSuites.insert(name, std::move(tests)); + } + + /** + * @brief Registers a test suite with explicit prerequisite dependencies. + * @param name Unique name of the test suite. + * @param prerequisites List of test suite names that must succeed before this suite runs. + * @param tests Collection of test cases belonging to this suite. + */ + inline auto addTestSuite(const std::string& name, std::vector prerequisites, std::vector tests) -> void + { + addTestSuite(name, tests); + for (const auto& prerequisite : prerequisites) + m_testSuites.addDependency(name, prerequisite); + } + + /** + * @brief Internal result structure containing the execution outcome and diagnostic message of a test case. + * @note Managed internally by TestRunner. Not intended to be instantiated directly by users. + */ + struct TestExecutionResult final + { + TestStatus status = TestStatus::Success; ///< Execution status outcome. + std::string message = {}; ///< Diagnostic message or skip reason. + std::chrono::nanoseconds duration = {}; ///< Measured test execution duration. + }; + + /** + * @brief Executes an individual test case in an isolated child process or worker thread. + * @param suiteName Name of the enclosing test suite. + * @param test Test case to run. + * @param settings Execution settings controlling timeouts and logging verbosity. + * @return Result outcome and diagnostics. + */ + inline auto executeTest(std::string_view suiteName, const TestCase& test, const TestSettings& settings) const -> TestExecutionResult + { + using namespace std::chrono_literals; + + const auto fullTestName = std::format("{}/{}", suiteName, test.name); + + if (settings.verbose) + { + Logger::emit<"separator">(); + Logger::print<"detail">("{}:", fullTestName); + } + Logger::waitUntilFinished(10s); + + const auto testStartTime = std::chrono::steady_clock::now(); + auto makeResult = [&](TestStatus status, std::string message = {}) -> TestExecutionResult { + const auto duration = std::chrono::duration_cast(std::chrono::steady_clock::now() - testStartTime); + if (settings.verbose and settings.chrono) + Logger::print<"detail">("Test duration: {}", Chrono::durationToString(duration)); + Logger::waitUntilFinished(10s); + return { + .status = status, + .message = std::move(message), + .duration = duration}; + }; + + // Workaround on Windows (MSVC ABI) + // https://github.com/llvm/llvm-project/issues/221609 +#if not defined(__linux__) + auto promise = std::promise{}; + auto future = promise.get_future(); + + auto worker = std::jthread{[promise = std::move(promise), function = test.function, testName = fullTestName](std::stop_token stopToken) mutable { + try + { + function(stopToken); + promise.set_value({TestStatus::Success, ""}); + } + catch (const TestSkipped& skipped) + { + promise.set_value({TestStatus::Skipped, skipped.what()}); + } + catch (const TestException& exception) + { + promise.set_value({TestStatus::Failure, std::format("The following test didn't pass:\n{}\n {}", testName, exception.what())}); + } + catch (const std::exception& exception) + { + promise.set_value({TestStatus::Failure, std::format("An exception occurred during the test:\n{}\n {}", testName, exception.what())}); + } + catch (...) + { + promise.set_value({TestStatus::Failure, std::format("An unknown exception occurred during the test: {}", testName)}); + } + }}; + + if (future.wait_for(test.timeout) == std::future_status::timeout) + { + worker.request_stop(); + worker.detach(); + const auto timeoutMessage = std::format("The following test timed out after {}s:\n{}", test.timeout.count(), fullTestName); + Logger::print<"error">("{}", timeoutMessage); + return makeResult(TestStatus::Failure, timeoutMessage); + } + + const auto result = future.get(); + if (result.status == TestStatus::Skipped) + { + if (settings.verbose) + Logger::print<"warning">("Skipped: {}", result.message); + return makeResult(TestStatus::Skipped, result.message); + } + if (result.status == TestStatus::Failure) + { + Logger::print<"error">("{}", result.message); + return makeResult(TestStatus::Failure, result.message); + } +#else + enum class ProcessResultType : std::uint8_t + { + Success = 0, + Skipped = 1, + Failure = 2 + }; + + auto channel = System::PipeChannel{}; + const auto processIdentifier = System::fork(); + if (processIdentifier < 0) + { + Logger::print<"error">("Failed to fork process for test: {}", fullTestName); + return makeResult(TestStatus::Failure, "fork() failed"); + } + + if (processIdentifier == 0) + { + channel.closeRead(); + Logger::reset(); + + auto writeResult = [&](ProcessResultType resultType, std::string_view message) { + channel.writeObject(resultType); + const auto messageLength = static_cast(std::ranges::size(message)); + channel.writeObject(messageLength); + if (messageLength > 0) + channel.write(message); + }; + + auto resultType = ProcessResultType::Success; + auto resultMessage = std::string{}; + + try + { + test.function(std::stop_token{}); + } + catch (const TestSkipped& skipped) + { + resultType = ProcessResultType::Skipped; + resultMessage = skipped.what(); + } + catch (const TestException& exception) + { + resultType = ProcessResultType::Failure; + resultMessage = std::format("The following test didn't pass:\n{}\n {}", fullTestName, exception.what()); + } + catch (const std::exception& exception) + { + resultType = ProcessResultType::Failure; + resultMessage = std::format("An exception occurred during the test:\n{}\n {}", fullTestName, exception.what()); + } + catch (...) + { + resultType = ProcessResultType::Failure; + resultMessage = std::format("An unknown exception occurred during the test: {}", fullTestName); + } + + Logger::waitUntilFinished(1s); + writeResult(resultType, resultMessage); + channel.closeWrite(); + System::quickExit(0); + } + + channel.closeWrite(); + + if (not channel.waitForData(std::chrono::duration_cast(test.timeout))) + { + System::kill(processIdentifier, System::Signal::Kill); + auto _ = System::wait(processIdentifier); + channel.closeRead(); + const auto timeoutMessage = std::format("The following test timed out after {}s:\n{}", test.timeout.count(), fullTestName); + Logger::print<"error">("{}", timeoutMessage); + return makeResult(TestStatus::Failure, timeoutMessage); + } + + if (const auto processStatus = System::wait(processIdentifier); + processStatus.isSignaled()) + { + channel.closeRead(); + const auto signalDescription = processStatus.getSignalDescription(); + const auto crashMessage = std::format("The following test crashed:\n{}\n Terminated by {}", fullTestName, signalDescription); + Logger::print<"error">("{}", crashMessage); + return makeResult(TestStatus::Failure, crashMessage); + } + + auto resultType = ProcessResultType::Failure; + auto messageLength = std::uint32_t{0}; + auto message = std::string{}; + + if (channel.readObject(resultType) and channel.readObject(messageLength)) + { + if (messageLength > 0) + { + message.resize(messageLength); + for (auto totalRead = 0uz; totalRead < messageLength;) + { + const auto bytesRead = channel.read(std::as_writable_bytes(std::span{std::ranges::data(message) + totalRead, messageLength - totalRead})); + if (bytesRead == 0) + break; + totalRead += bytesRead; + } + } + } + channel.closeRead(); + + if (resultType == ProcessResultType::Skipped) + { + if (settings.verbose) + Logger::print<"warning">("Skipped: {}", message); + return makeResult(TestStatus::Skipped, message); + } + + if (resultType == ProcessResultType::Failure) + { + Logger::print<"error">("{}", message); + return makeResult(TestStatus::Failure, message); + } +#endif + if (settings.verbose) + Logger::print<"success">("Passed"); + return makeResult(TestStatus::Success, ""); + } + + /** + * @brief Executes all registered test suites respecting dependency order and configuration settings. + * @param settings Execution settings controlling verbose output, filtering, and abort conditions. + * @return Exit code: 0 on success, non-zero if any test suite failed. + */ + inline auto executeTests(TestSettings settings) -> int + { + using namespace std::chrono_literals; + + if (settings.abort) + return exitSuccess; + + const auto totalExecutionStartTime = std::chrono::steady_clock::now(); + auto testSuiteIsSuccess = std::unordered_map{}; + + auto activeSuites = std::unordered_set{}; + if (not std::ranges::empty(settings.filter)) + { + for (const auto& [suiteName, node] : m_testSuites.nodes) + if (suiteName.contains(settings.filter)) + { + activeSuites.insert(suiteName); + for (auto toVisit = std::vector{std::ranges::begin(node.dependencies), std::ranges::end(node.dependencies)}; + not std::ranges::empty(toVisit);) + { + auto current = toVisit.back(); + toVisit.pop_back(); + if (activeSuites.insert(current).second) + if (const auto it = m_testSuites.nodes.find(current); it != std::ranges::end(m_testSuites.nodes)) + toVisit.insert(std::ranges::end(toVisit), std::ranges::begin(it->second.dependencies), std::ranges::end(it->second.dependencies)); + } + } + + if (std::ranges::empty(activeSuites)) + { + Logger::print<"warning">("No test suites matched filter '{}'", settings.filter); + return exitSuccess; + } + } + + const auto nbTests = std::ranges::fold_left( + m_testSuites.nodes | std::views::filter([&](const auto& pair) { + return std::ranges::empty(settings.filter) or activeSuites.contains(pair.first); + }) | std::views::transform([](auto&& pair) { return std::ranges::size(pair.second.value); }), + 0uz, + std::plus{}); + Logger::print<"info">("{} tests found. Execution:", nbTests); + + auto failedTestSuites = std::vector{}; + auto implicitlySkippedTestSuites = std::vector{}; + auto explicitlySkippedTests = std::vector{}; + auto recordedSuites = std::vector{}; + + auto nbSuccess = 0uz; + auto nbFail = 0uz; + auto nbSkip = 0uz; + + if (auto result = m_testSuites.forEach([&](const auto& name, const auto& tests, const auto& dependencies) -> bool { + if (not std::ranges::empty(settings.filter) and not activeSuites.contains(name)) + return true; + + auto currentRecordedSuite = RecordedTestSuite{.name = name, .testCases = {}, .duration = {}}; + const auto suiteStartTime = std::chrono::steady_clock::now(); + + auto allTestsPassed = true; + + if (std::ranges::empty(tests)) + { + Logger::print<"warning">("No tests found in test suite {}", name); + testSuiteIsSuccess[name] = true; + } + else if (const auto failedDependency = std::ranges::find_if(dependencies, [&](const auto& dependency) { return not testSuiteIsSuccess[dependency]; }); + failedDependency != std::ranges::end(dependencies)) + { + Logger::print<"warning">("Skipping test suite {} due to failed prerequisites in test suite {}", name, *failedDependency); + testSuiteIsSuccess[name] = false; + allTestsPassed = false; + implicitlySkippedTestSuites.push_back(name); + nbSkip += std::ranges::size(tests); + for (const auto& test : tests) + currentRecordedSuite.testCases.push_back(RecordedTestCase{ + .name = test.name, + .className = name, + .status = TestStatus::Skipped, + .message = std::format("Skipped due to failed prerequisite '{}'", *failedDependency), + .duration = 0ns}); + } + else + { + for (const auto& test : tests) + { + const auto [status, message, testDuration] = executeTest(name, test, settings); + currentRecordedSuite.testCases.push_back(RecordedTestCase{ + .name = test.name, + .className = name, + .status = status, + .message = message, + .duration = testDuration}); + + if (status == TestStatus::Success) + ++nbSuccess; + else if (status == TestStatus::Skipped) + { + ++nbSkip; + explicitlySkippedTests.push_back({std::format("{}/{}", name, test.name), message}); + } + else + { + ++nbFail; + allTestsPassed = false; + if (settings.failFast) + break; + } + } + + testSuiteIsSuccess[name] = allTestsPassed; + if (not allTestsPassed) + failedTestSuites.push_back(name); + } + + currentRecordedSuite.duration = std::chrono::duration_cast(std::chrono::steady_clock::now() - suiteStartTime); + recordedSuites.push_back(std::move(currentRecordedSuite)); + return allTestsPassed or not settings.failFast; + }); + not result) + Logger::print<"error">("The tests cannot be run: {}", result.error()); + + const auto totalDuration = std::chrono::duration_cast(std::chrono::steady_clock::now() - totalExecutionStartTime); + if (not std::ranges::empty(settings.report)) + writeReport(settings.report, recordedSuites, totalDuration); + + Logger::emit<"separator">(); + Logger::print<"detail">("Test results"); + if (nbFail == 0 and nbSkip == 0) + { + Logger::print<"success">("All tests passed successfully"); + Logger::waitUntilFinished(10s); + return exitSuccess; + } + if (nbFail > 0) + Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Red, "The tests failed:"); + else + Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Yellow, "Some tests were skipped:"); + + if (nbSuccess > 0) + Logger::print<"success">("- {} successful tests", nbSuccess); + if (nbSuccess == 0) + Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Red, "- 0 successful tests"); + + if (nbFail > 0) + { + Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Red, std::format("- {} failed tests", nbFail)); + for (const auto& failedTestSuite : failedTestSuites) + Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Red, std::format(" - {}", failedTestSuite)); + } + + if (nbSkip > 0) + { + Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Yellow, std::format("- {} skipped tests", nbSkip)); + for (const auto& skippedTest : explicitlySkippedTests) + Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Yellow, std::format(" - {}: {}", skippedTest.name, skippedTest.message)); + for (const auto& skippedTestSuite : implicitlySkippedTestSuites) + Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Yellow, std::format(" - {} (due to failed prerequisites)", skippedTestSuite)); + } + + Logger::waitUntilFinished(10s); + return nbFail == 0 ? exitSuccess : exitFailure; + } + + private: + static inline auto writeReport( + std::string_view reportConfiguration, + std::span suites, + std::chrono::nanoseconds totalDuration) -> void + { + if (std::ranges::empty(reportConfiguration)) + return; + + auto format = std::string_view{"junit"}; + auto path = reportConfiguration; + if (const auto colon = reportConfiguration.find(':'); colon != std::string_view::npos) + { + format = reportConfiguration.substr(0, colon); + path = reportConfiguration.substr(colon + 1); + } + + if (format == "junit" or path.ends_with(".xml")) + JUnit::writeJUnitXmlReport(path, suites, totalDuration); + else + Logger::print<"warning">("Unknown report format: {}", format); + } + + Container::DependencyGraph> m_testSuites; + }; + + /// @brief Global test runner instance used for test suite registration and execution. + inline auto testRunner = TestRunner{}; +} diff --git a/modules/UnitTest/UnitTest.mpp b/modules/UnitTest/UnitTest.mpp index 4e5b50f6..80d9acdf 100644 --- a/modules/UnitTest/UnitTest.mpp +++ b/modules/UnitTest/UnitTest.mpp @@ -1,484 +1,807 @@ module; -#include +#if not defined(_WIN32) +# include +#endif export module CppUtils.UnitTest; import std; -import CppUtils.Chrono.Concept; -import CppUtils.ChronoLogger; +import CppUtils.Chrono; import CppUtils.Container; import CppUtils.Logger; import CppUtils.Log.Exception; import CppUtils.Log.Config.UnitTests; import CppUtils.String; import CppUtils.Terminal; -import CppUtils.Thread.Poll; -import CppUtils.System.Main; - +import CppUtils.System; +import CppUtils.Math.Utility; +import CppUtils.Math.Random; +import CppUtils.Language.Parameters; +import CppUtils.Thread; +import CppUtils.Type; + +export import CppUtils.UnitTest.Assertion; +export import CppUtils.UnitTest.Benchmark; +export import CppUtils.UnitTest.EventWatcher; +export import CppUtils.UnitTest.Expectation; +export import CppUtils.UnitTest.JUnitFormatter; export import CppUtils.UnitTest.LifetimeChecker; +export import CppUtils.UnitTest.Report; +export import CppUtils.UnitTest.Runner; namespace CppUtils::UnitTest { - export struct TestSettings final - { - bool verbose = true; - bool detail = true; - bool chrono = false; - bool fastAbort = false; - }; - - struct Test final - { - std::string name; - std::function function; - std::chrono::seconds timeout = std::chrono::seconds{10}; - }; - - // Workaround on Windows (MSVC ABI) - // https://github.com/llvm/llvm-project/issues/221609 -#if defined(_WIN32) - export class TestException: public std::exception + /** + * @brief Core test suite execution context providing assertion methods, builders, and test registration. + * + * Instances are passed to test suite definitions configured via `testSuite("SuiteName") = [](TestSuite& suite) { ... };`. + * Supports regular tests, compile-time tests, parameterized tests (types, values, ranges), fuzz tests, and benchmarks. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/ (Guide des tests unitaires) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/ (Unit test guide) + */ + export struct TestSuite final { - public: - explicit inline TestException(std::string message): - m_message{std::move(message)} - {} - - [[nodiscard]] inline auto what() const noexcept -> const char* override - { - return m_message.c_str(); - } + using Logger = Logger<"UnitTests">; private: - std::string m_message; - }; - - class TestSkipped final: public std::exception - { - public: - explicit inline TestSkipped(std::string message, std::source_location location = std::source_location::current()): - m_message{std::format("{} (at {}:{})", message, location.file_name(), location.line())} - {} - - [[nodiscard]] inline auto what() const noexcept -> const char* override + friend struct TestSuiteBuilder; + friend struct TestBuilder; + friend struct FuzzTestBuilder; + template + friend struct TypedTestSuiteBuilder; + template + friend struct ParameterizedTestSuiteBuilder; + + inline explicit TestSuite(const std::string& name, std::vector prerequisites, auto&& function) { - return m_message.c_str(); + function(*this); + testRunner.addTestSuite(name, prerequisites, std::move(tests)); } - private: - std::string m_message; - }; -#else - export class TestException: public std::runtime_error - { - public: - inline TestException(std::string message): - std::runtime_error{std::move(message)} - {} - }; - - class TestSkipped final: public std::runtime_error - { - public: - explicit inline TestSkipped(std::string message, std::source_location location = std::source_location::current()): - std::runtime_error{std::format("{} (at {}:{})", message, location.file_name(), location.line())} - {} - }; -#endif - - struct SkippedTest final - { - std::string name; - std::string message; - }; - - enum class TestStatus - { - Success, - Failure, - Skipped - }; - - class TestRunner final - { - using Logger = Logger<"UnitTests">; - - public: - inline auto addTestSuite(const std::string& name, std::vector tests) -> void + inline auto addTest(std::string name, std::function function, std::chrono::seconds timeout = std::chrono::seconds{10}) -> void { - for (auto& test : tests) - test.name = std::format("{}/{}", name, test.name); - m_testSuites.insert(name, std::move(tests)); + tests.emplace_back(std::move(name), std::move(function), timeout); } - inline auto addTestSuite(const std::string& name, std::vector prerequisites, std::vector tests) -> void + inline auto addTest(std::string name, std::function function, std::chrono::seconds timeout = std::chrono::seconds{10}) -> void { - addTestSuite(name, tests); - for (const auto& prerequisite : prerequisites) - m_testSuites.addDependency(name, prerequisite); + tests.emplace_back(std::move(name), [function = std::move(function)](std::stop_token) { function(); }, timeout); } - inline auto executeTest(const Test& test, const TestSettings& settings) const -> std::pair + static constexpr auto forEachParameterValue(const auto& container, auto&& invocable) { - using namespace std::chrono_literals; - if (settings.verbose) + if constexpr (Type::Specializes, std::tuple>) { - Logger::emit<"separator">(); - Logger::print<"detail">("{}:", test.name); - Logger::waitUntilFinished(10s); + std::apply([&](const auto&... values) { + (invocable(values), ...); + }, container); } - - // Workaround on Windows (MSVC ABI) - // https://github.com/llvm/llvm-project/issues/221609 -#if defined(_WIN32) - struct ExecutionResult final + else { - TestStatus status = TestStatus::Success; - std::string message; - }; - - auto chronoLogger = Log::ChronoLogger{[](std::string_view duration) { - Logger::print<"detail">("Test duration: {}", duration); - }, settings.verbose and settings.chrono}; - - auto promise = std::promise{}; - auto future = promise.get_future(); + for (const auto& value : container) + invocable(value); + } + } - auto worker = std::jthread{[promise = std::move(promise), function = test.function, testName = test.name](std::stop_token stopToken) mutable { - try - { - function(stopToken); - promise.set_value({TestStatus::Success, ""}); - } - catch (const TestSkipped& skipped) - { - promise.set_value({TestStatus::Skipped, skipped.what()}); - } - catch (const TestException& exception) - { - promise.set_value({TestStatus::Failure, std::format("The following test didn't pass:\n{}\n {}", testName, exception.what())}); - } - catch (const std::exception& exception) - { - promise.set_value({TestStatus::Failure, std::format("An exception occurred during the test:\n{}\n {}", testName, exception.what())}); - } - catch (...) - { - promise.set_value({TestStatus::Failure, std::format("An unknown exception occurred during the test: {}", testName)}); - } - }}; + static auto formatParameterName(std::string_view baseName, const auto& value, std::size_t& index) -> std::string + { + if constexpr (requires { std::format("{}", value); }) + return std::format("{} [{}]", baseName, value); + else + return std::format("{} [value {}]", baseName, index++); + } - if (future.wait_for(test.timeout) == std::future_status::timeout) + public: + /** + * @brief Base CRTP helper providing timeout configuration for test builders. + * @tparam Derived Derived builder type for method chaining. + */ + template + struct TimeoutBuilder + { + std::chrono::seconds testTimeout{10}; ///< Maximum execution timeout duration. + + /** + * @brief Sets the execution timeout before the test is aborted. + * @param newTimeout Timeout duration. + * @return Reference to this builder. + */ + inline auto timeout(std::chrono::seconds newTimeout) -> Derived& { - worker.request_stop(); - worker.detach(); - Logger::print<"error">("The following test timed out after {}s:\n{}", test.timeout.count(), test.name); - return {TestStatus::Failure, ""}; + testTimeout = newTimeout; + return static_cast(*this); } - - const auto [status, message] = future.get(); - if (status == TestStatus::Skipped) + }; + + /** + * @brief Builder for registering type-parameterized tests within a TestSuite. + * @note Returned by TestBuilder::forTypes(); not intended to be instantiated directly by user code. + * @tparam Types Type list to instantiate the test with. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#itération-sur-une-liste-de-types-fortypes (Tests paramétrés par types) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#iterating-over-a-list-of-types-fortypes (Type-parameterized tests) + */ + template + struct TypedTestBuilder final: TimeoutBuilder> + { + using TimeoutBuilder>::testTimeout; + + TestSuite& parentSuite; ///< Reference to the enclosing test suite registering the tests. + std::string testName; ///< Identifier of the test case. + + TypedTestBuilder(std::chrono::seconds timeout, TestSuite& suite, std::string name): + TimeoutBuilder>{timeout}, + parentSuite{suite}, + testName{std::move(name)} + {} + + /** + * @brief Registers the test implementation callback invoked for each type. + * @param function Invocable accepting either a template type argument or `std::type_identity`. + */ + inline auto operator=(auto&& function) -> void { - if (settings.verbose) - Logger::print<"warning">("Skipped: {}", message); - return {TestStatus::Skipped, message}; + auto index = 0uz; + auto registerType = [&]() { + auto name = std::format("{} [type {}]", testName, index++); + if constexpr (requires { function.template operator()(); }) + parentSuite.addTest(std::move(name), [function] { + function.template operator()(); + }, testTimeout); + else if constexpr (requires { function(std::type_identity{}); }) + parentSuite.addTest(std::move(name), [function] { + function(std::type_identity{}); + }, testTimeout); + else if constexpr (requires (std::stop_token token) { function.template operator()(token); }) + parentSuite.addTest(std::move(name), [function](std::stop_token token) { + function.template operator()(token); + }, testTimeout); + else if constexpr (requires (std::stop_token token) { function(std::type_identity{}, token); }) + parentSuite.addTest(std::move(name), [function](std::stop_token token) { + function(std::type_identity{}, token); + }, testTimeout); + }; + (registerType.template operator()(), ...); } - if (status == TestStatus::Failure) + }; + + /** + * @brief Builder for registering a test parameterized over values (std::tuple or input range). + * @note Returned by TestBuilder::forValues(); not intended to be instantiated or manipulated directly by user code. + * @tparam ValuesContainer Container holding parameter values (std::tuple or input range). + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#itération-sur-une-liste-de-valeurs-forvalues (Tests paramétrés par valeurs) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#iterating-over-a-list-of-values-forvalues (Value-parameterized tests) + */ + template + struct ParameterizedTestBuilder final: TimeoutBuilder> + { + using TimeoutBuilder>::testTimeout; + + TestSuite& parentSuite; ///< Reference to the enclosing test suite registering the tests. + std::string testName; ///< Identifier of the test case. + ValuesContainer values; ///< Discrete values tuple or range of values. + + ParameterizedTestBuilder(std::chrono::seconds timeout, TestSuite& suite, std::string name, ValuesContainer valuesContainer): + TimeoutBuilder>{timeout}, + parentSuite{suite}, + testName{std::move(name)}, + values{std::move(valuesContainer)} + {} + + /** + * @brief Registers the test implementation callback invoked for each parameter value. + * @param function Invocable accepting the parameter value. + */ + inline auto operator=(auto&& function) -> void { - Logger::print<"error">("{}", message); - return {TestStatus::Failure, ""}; + auto index = 0uz; + forEachParameterValue(values, [&](const auto& value) { + auto name = formatParameterName(testName, value, index); + if constexpr (requires { function(value); }) + parentSuite.addTest(std::move(name), [function, value] { + function(value); + }, testTimeout); + else if constexpr (requires (std::stop_token token) { function(value, token); }) + parentSuite.addTest(std::move(name), [function, value](std::stop_token token) { + function(value, token); + }, testTimeout); + }); } -#else - try + }; + + template + using ValueTestBuilder = ParameterizedTestBuilder>; + + template + using RangeTestBuilder = ParameterizedTestBuilder; + + /** + * @brief Builder for configuring and registering an individual test case. + * @note Returned by TestSuite::test(std::string); not intended to be instantiated directly by user code. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#déclarer-une-suite-de-tests (Déclarer une suite de tests) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#declaring-a-test-suite (Declaring a test suite) + */ + struct TestBuilder final: TimeoutBuilder + { + TestSuite& parentSuite; ///< Reference to the enclosing test suite registering the test. + std::string testName; ///< Identifier of the test case. + + TestBuilder(std::chrono::seconds timeout, TestSuite& suite, std::string name): + TimeoutBuilder{timeout}, + parentSuite{suite}, + testName{std::move(name)} + {} + + /** + * @brief Converts this test into a type-parameterized test across the specified types. + * @tparam Types Type parameters to instantiate the test with. + * @return TypedTestBuilder instance. + */ + template + [[nodiscard]] inline auto forTypes() -> TypedTestBuilder { - try - { - auto chronoLogger = Log::ChronoLogger{[](std::string_view duration) { - Logger::print<"detail">("Test duration: {}", duration); - }, settings.verbose and settings.chrono}; - - auto promise = std::promise{}; - auto future = promise.get_future(); - - auto worker = std::jthread{[promise = std::move(promise), function = test.function](std::stop_token stopToken) mutable { - try - { - function(stopToken); - promise.set_value(); - } - catch (...) - { - promise.set_exception(std::current_exception()); - } - }}; - - if (future.wait_for(test.timeout) == std::future_status::timeout) - { - worker.request_stop(); - worker.detach(); - throw TestException{std::format("Test timed out after {}s", test.timeout.count())}; - } - - future.get(); - } - catch (const TestSkipped& skipped) - { - if (settings.verbose) - Logger::print<"warning">("Skipped: {}", skipped.what()); - return {TestStatus::Skipped, skipped.what()}; - } - catch ([[maybe_unused]] const TestException& exception) - { - std::throw_with_nested(std::runtime_error{std::format("The following test didn't pass:\n{}", test.name)}); - } - catch (const std::exception& exception) - { - std::throw_with_nested(std::runtime_error{std::format("An exception occurred during the test:\n{}\n{}", test.name, exception.what())}); - } + return TypedTestBuilder{testTimeout, parentSuite, std::move(testName)}; } - catch (const std::exception& exception) + + /** + * @brief Converts this test into a value-parameterized test across discrete values or a range. + * @tparam Values Value types or single range type. + * @param values Values or range to iterate over. + * @return ParameterizedTestBuilder instance. + */ + template + [[nodiscard]] inline auto forValues(Values&&... values) { - logException(exception); - return {TestStatus::Failure, ""}; + if constexpr (sizeof...(Values) == 1 and (std::ranges::input_range> and ...)) + return ParameterizedTestBuilder...>{ + testTimeout, parentSuite, std::move(testName), (std::forward(values), ...)}; + else + return ParameterizedTestBuilder...>>{ + testTimeout, parentSuite, std::move(testName), std::make_tuple(std::forward(values)...)}; } -#endif - if (settings.verbose) - Logger::print<"success">("Passed"); - Logger::waitUntilFinished(10s); - return {TestStatus::Success, ""}; - } - inline auto executeTests(TestSettings settings) -> int + /** + * @brief Assigns the test body callback and registers the test into the parent suite. + * @param function Invocable containing test logic (accepts optional std::stop_token). + */ + inline auto operator=(auto&& function) -> void + { + if constexpr (std::is_invocable_v) + parentSuite.addTest(std::move(testName), std::forward(function), testTimeout); + else + parentSuite.addTest(std::move(testName), [function = std::forward(function)] { function(); }, testTimeout); + } + }; + + /** + * @brief Builder for configuring and registering a randomized fuzz test. + * @note Returned by TestSuite::fuzzTest(std::string, std::size_t); not intended to be instantiated directly by user code. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#tests-de-robustesse-et-fuzzing-fuzztest-et-fuzz (Tests de robustesse et fuzzing) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#robustness-testing-and-fuzzing-fuzztest-and-fuzz (Fuzz testing) + */ + struct FuzzTestBuilder final: TimeoutBuilder { - using namespace std::chrono_literals; - auto testSuiteIsSuccess = std::unordered_map{}; - - const auto nbTests = std::ranges::fold_left( - std::views::values(m_testSuites.nodes) | std::views::transform([](auto&& node) { return std::ranges::size(node.value); }), - 0uz, - std::plus{}); - Logger::print<"info">("{} tests found. Execution:", nbTests); - - auto failedTestSuites = std::vector{}; - auto implicitlySkippedTestSuites = std::vector{}; - auto explicitlySkippedTests = std::vector{}; - - auto nbSuccess = 0uz; - auto nbFail = 0uz; - auto nbSkip = 0uz; - - if (auto result = m_testSuites.forEach([&](const auto& name, const auto& tests, const auto& dependencies) -> bool { - if (std::ranges::empty(tests)) - { - Logger::print<"warning">("No tests found in test suite {}", name); - testSuiteIsSuccess[name] = true; - return not settings.fastAbort; - } - - for (const auto& dependency : dependencies) - if (not testSuiteIsSuccess[dependency]) - { - Logger::print<"warning">("Skipping test suite {} due to failed prerequisites in test suite {}", name, dependency); - testSuiteIsSuccess[name] = false; - implicitlySkippedTestSuites.push_back(name); - nbSkip += std::ranges::size(tests); - return not settings.fastAbort; - } + TestSuite& parentSuite; ///< Reference to the enclosing test suite registering the test. + std::string testName; ///< Identifier of the test case. + std::size_t testIterations; ///< Number of randomized iterations to execute. + std::uint64_t testSeed{Math::DeterministicRandomEngine::defaultSeed}; ///< Deterministic pseudo-random number generator initial seed. + + /** + * @brief Sets the deterministic initial pseudo-random number generator seed for fuzz reproducibility. + * @param newSeed Seed value. + * @return Reference to this builder. + */ + inline auto seed(std::uint64_t newSeed) -> FuzzTestBuilder& + { + testSeed = newSeed; + return *this; + } - auto allTestsPassed = true; - for (const auto& test : tests) - { - const auto [status, skipMessage] = executeTest(test, settings); - if (status == TestStatus::Success) - ++nbSuccess; - else if (status == TestStatus::Skipped) - { - ++nbSkip; - explicitlySkippedTests.push_back({test.name, skipMessage}); - } - else + /** + * @brief Registers the fuzz test callback executed for the configured iteration count. + * @param function Invocable accepting a DeterministicRandomEngine&. + */ + inline auto operator=(auto&& function) -> void + { + parentSuite.addTest(std::move(testName), [iterations = testIterations, function = std::forward(function), seed = testSeed](std::stop_token stopToken) { + auto fuzzer = Math::DeterministicRandomEngine{seed}; + for (auto _ : std::views::iota(0uz, iterations)) { - ++nbFail; - allTestsPassed = false; - if (settings.fastAbort) + if (stopToken.stop_requested()) break; + function(fuzzer); } - } - - testSuiteIsSuccess[name] = allTestsPassed; - if (not allTestsPassed) - failedTestSuites.push_back(name); - - return allTestsPassed or not settings.fastAbort; - }); - not result) - Logger::print<"error">("The tests cannot be run: {}", result.error()); - - Logger::emit<"separator">(); - Logger::print<"detail">("Test results"); - if (nbFail == 0 and nbSkip == 0) - { - Logger::print<"success">("All tests passed successfully"); - return exitSuccess; + }, testTimeout); } - if (nbFail > 0) - Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Red, "The tests failed:"); - else - Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Yellow, "Some tests were skipped:"); - - if (nbSuccess > 0) - Logger::print<"success">("- {} successful tests", nbSuccess); - if (nbSuccess == 0) - Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Red, "- 0 successful tests"); + }; + + /** + * @brief Declares a test case with the specified name, returning a builder to configure and assign its body. + * @param name Descriptive test name. + * @return TestBuilder instance. + */ + [[nodiscard]] inline auto test(std::string name) -> TestBuilder + { + return TestBuilder{std::chrono::seconds{10}, *this, std::move(name)}; + } - if (nbFail > 0) + /** + * @brief Evaluates a test predicate at compile time via static_assert and registers it as a passed test. + * + * The template predicate is evaluated at compile time: any assertion failure immediately breaks + * the build before the test executable is even run. + * + * At runtime, this method registers an empty test body in the test runner so that the test appears + * in reports and metrics while avoiding the process isolation fork() overhead of runtime test cases. + * Prefer this overload when verifying constexpr functions, concepts, type traits, or template metaprogramming. + * + * @tparam Predicate Constexpr invocable returning a boolean or performing static assertions. + * @param name Descriptive test name. + */ + template + inline auto test(std::string name) -> void + { + if constexpr (requires { { Predicate() } -> std::convertible_to; }) { - Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Red, std::format("- {} failed tests", nbFail)); - for (const auto& failedTestSuite : failedTestSuites) - Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Red, std::format(" - {}", failedTestSuite)); + static_assert(Predicate(), "Compile-time test assertion evaluated to false"); } - - if (nbSkip > 0) + else { - Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Yellow, std::format("- {} skipped tests", nbSkip)); - for (const auto& skippedTest : explicitlySkippedTests) - Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Yellow, std::format(" - {}: {}", skippedTest.name, skippedTest.message)); - for (const auto& skippedTestSuite : implicitlySkippedTestSuites) - Logger::emit<"color">(Terminal::TextColor::TextColorEnum::Yellow, std::format(" - {} (due to failed prerequisites)", skippedTestSuite)); + constexpr auto runTest = [] { + Predicate(); + return true; + }; + static_assert(runTest(), "Compile-time test failed"); } + addTest(std::move(name), [] {}); + } - Logger::waitUntilFinished(10s); - return nbFail == 0 ? exitSuccess : exitFailure; + /** + * @brief Registers a fuzz test executed for a specified number of randomized iterations. + * @param name Test identifier. + * @param iterations Number of iterations to run. + * @return FuzzTestBuilder for assigning the test body callback. + */ + [[nodiscard]] inline auto fuzzTest(std::string name, std::size_t iterations) -> FuzzTestBuilder + { + return FuzzTestBuilder{.parentSuite = *this, .testName = std::move(name), .testIterations = iterations}; } - private: - Container::DependencyGraph> m_testSuites; + std::vector tests; }; - inline /* constinit */ auto testRunner = TestRunner{}; - - export struct TestSuite final + /** + * @brief Tag token returned by test suite registration assignments. + * @note Returned upon assigning a suite callback (e.g. via TestSuiteBuilder::operator=); not intended to be instantiated, stored, or managed directly by user code. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#déclarer-une-suite-de-tests (Déclarer une suite de tests) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#declaring-a-test-suite (Declaring a test suite) + */ + export struct TestSuiteRegistration final + {}; + + /** + * @brief Builder for registering an entire test suite parameterized over a list of types. + * @note Returned by TestSuiteBuilder::forTypes(); not intended to be instantiated directly by user code. + * @tparam Types Type parameters instantiated for each suite instance. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#paramétrage-au-niveau-dune-suite-complète (Suites de tests paramétrées) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#parameterization-on-a-complete-suite (Parameterized test suites) + */ + export template + struct TypedTestSuiteBuilder final { - using Logger = Logger<"UnitTests">; - - inline explicit TestSuite(const std::string& name, std::vector prerequisites, std::function function) + std::string name; ///< Identifier of the test suite. + std::vector prerequisites; ///< Names of prerequisite test suites that must succeed before this suite runs. + + /** + * @brief Registers the suite callback instantiated for each specified type. + * @param function Invocable accepting (TestSuite&) and optional `std::type_identity`. + * @return TestSuiteRegistration token. + */ + inline auto operator=(auto&& function) -> TestSuiteRegistration { - function(*this); - testRunner.addTestSuite(name, prerequisites, std::move(tests)); + auto index = 0uz; + auto registerType = [&]() { + auto suiteName = std::format("{} [type {}]", name, index++); + auto registeredSuite = TestSuite{std::move(suiteName), prerequisites, [function](TestSuite& suite) { + if constexpr (requires { function.template operator()(suite); }) + function.template operator()(suite); + else if constexpr (requires { function(suite, std::type_identity{}); }) + function(suite, std::type_identity{}); + }}; + }; + (registerType.template operator()(), ...); + return TestSuiteRegistration{}; } + }; - inline explicit TestSuite(std::string name, std::function function) + /** + * @brief Builder for registering an entire test suite parameterized over values (std::tuple or input range). + * @note Returned by TestSuiteBuilder::forValues(); not intended to be instantiated or manipulated directly by user code. + * @tparam ValuesContainer Container holding parameter values (std::tuple or input range). + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#paramétrage-au-niveau-dune-suite-complète (Suites de tests paramétrées) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#parameterization-on-a-complete-suite (Parameterized test suites) + */ + export template + struct ParameterizedTestSuiteBuilder final + { + std::string name; ///< Identifier of the test suite. + std::vector prerequisites; ///< Names of prerequisite test suites that must succeed before this suite runs. + ValuesContainer values; ///< Discrete values tuple or range of values. + + /** + * @brief Registers the suite callback instantiated for each parameter value. + * @param function Invocable accepting (TestSuite&, const Value&). + * @return TestSuiteRegistration token. + */ + inline auto operator=(auto&& function) -> TestSuiteRegistration { - function(*this); - testRunner.addTestSuite(name, std::move(tests)); + auto index = 0uz; + TestSuite::forEachParameterValue(values, [&](const auto& value) { + auto suiteName = TestSuite::formatParameterName(name, value, index); + auto registeredSuite = TestSuite{std::move(suiteName), prerequisites, [function, value](TestSuite& suite) { + function(suite, value); + }}; + }); + return TestSuiteRegistration{}; } + }; - inline auto addTest(std::string name, std::function function, std::chrono::seconds timeout = std::chrono::seconds{10}) -> void + /** + * @brief Builder for registering a test suite parameterized over a discrete pack of values. + * @note Returned by TestSuiteBuilder::forValues(...); not intended to be instantiated or manipulated directly by user code. + * + * Wraps the variadic parameter pack into a `std::tuple` container passed to ParameterizedTestSuiteBuilder. + * + * @tparam Values Types of the individual parameter values. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#paramétrage-au-niveau-dune-suite-complète (Suites de tests paramétrées) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#parameterization-on-a-complete-suite (Parameterized test suites) + */ + export template + using ValueTestSuiteBuilder = ParameterizedTestSuiteBuilder>; + + /** + * @brief Builder for registering a test suite parameterized over an input range. + * @note Returned by TestSuiteBuilder::forValues(range); not intended to be instantiated or manipulated directly by user code. + * + * Evaluates each element of the provided range as an independent test suite parameter. + * + * @tparam Range Type satisfying std::ranges::input_range. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#paramétrage-au-niveau-dune-suite-complète (Suites de tests paramétrées) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#parameterization-on-a-complete-suite (Parameterized test suites) + */ + export template + using RangeTestSuiteBuilder = ParameterizedTestSuiteBuilder; + + /** + * @brief Top-level builder used to declare a test suite, configure dependencies, and define its body. + * @note Returned by testSuite(std::string); not intended to be instantiated directly by user code. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#déclarer-une-suite-de-tests (Déclarer une suite de tests) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#declaring-a-test-suite (Declaring a test suite) + */ + export struct TestSuiteBuilder final + { + std::string name; ///< Identifier of the test suite. + std::vector prerequisites; ///< Names of prerequisite test suites that must succeed before this suite runs. + + /** + * @brief Adds prerequisite test suites that must succeed before executing this suite. + * @param dependencies Prerequisite test suite names. + * @return Reference to this builder. + */ + template + requires (std::is_convertible_v and ...) + inline auto dependsOn(Dependencies&&... dependencies) -> TestSuiteBuilder& { - tests.emplace_back(std::move(name), std::move(function), timeout); + (prerequisites.emplace_back(std::forward(dependencies)), ...); + return *this; } - inline auto addTest(std::string name, std::function function, std::chrono::seconds timeout = std::chrono::seconds{10}) -> void + /** + * @brief Converts this suite into a type-parameterized test suite. + * @tparam Types Type parameter list. + * @return TypedTestSuiteBuilder instance. + */ + template + [[nodiscard]] inline auto forTypes() -> TypedTestSuiteBuilder { - tests.emplace_back(std::move(name), [function = std::move(function)](std::stop_token) { function(); }, timeout); + return TypedTestSuiteBuilder{.name = std::move(name), .prerequisites = std::move(prerequisites)}; } - [[noreturn]] inline auto skip(std::string message, std::source_location sourceLocation = std::source_location::current()) -> void + /** + * @brief Converts this suite into a value-parameterized test suite over values or a range. + * @tparam Values Value types or single range type. + * @param values Values or range to parameterize the suite with. + * @return ParameterizedTestSuiteBuilder instance. + */ + template + [[nodiscard]] inline auto forValues(Values&&... values) { - throw TestSkipped{std::move(message), sourceLocation}; + if constexpr (sizeof...(Values) == 1 and (std::ranges::input_range> and ...)) + return ParameterizedTestSuiteBuilder...>{ + .name = std::move(name), + .prerequisites = std::move(prerequisites), + .values = (std::forward(values), ...)}; + else + return ParameterizedTestSuiteBuilder...>>{ + .name = std::move(name), + .prerequisites = std::move(prerequisites), + .values = std::make_tuple(std::forward(values)...)}; } - inline auto skipIf(bool condition, std::string message, std::source_location sourceLocation = std::source_location::current()) -> void + /** + * @brief Registers the test suite callback into the global test runner. + * @param function Invocable accepting a TestSuite&. + * @return TestSuiteRegistration token. + */ + inline auto operator=(auto&& function) -> TestSuiteRegistration { - if (condition) - skip(std::move(message), sourceLocation); + auto registeredSuite = TestSuite{std::move(name), std::move(prerequisites), std::forward(function)}; + return TestSuiteRegistration{}; } + }; - // Todo C++23: std::stacktrace stacktrace = std::current_stacktrace() - inline auto expect(const auto& condition, std::source_location sourceLocation = std::source_location::current()) -> void - requires requires { static_cast(condition); } - { - if (not static_cast(condition)) [[unlikely]] - throw TestException{std::format("In {}\nAt line {}, column {}\nIn expect(condition)", - sourceLocation.file_name(), - sourceLocation.line(), - sourceLocation.column())}; - } + /** + * @brief Declares a test suite. + * + * @param name Descriptive test suite identifier. + * @return TestSuiteBuilder used to configure dependencies or assign suite body. + * + * @see https://morgancaron.github.io/CppUtils/fr/guides/unit-test/#déclarer-une-suite-de-tests (Guide des tests unitaires) + * @see https://morgancaron.github.io/CppUtils/en/guides/unit-test/#declaring-a-test-suite (Unit test guide) + */ + export [[nodiscard]] inline auto testSuite(std::string name) -> TestSuiteBuilder + { + return TestSuiteBuilder{.name = std::move(name), .prerequisites = {}}; + } - template - inline auto expectWait( - auto&& predicate, - TimeoutDuration timeout = std::chrono::milliseconds{1'000}, - StepDuration step = std::chrono::milliseconds{20}, - std::source_location sourceLocation = std::source_location::current()) -> void - { - if (not Thread::pollFor(std::forward(predicate), timeout, step)) [[unlikely]] - throw TestException{std::format("In {}\nAt line {}, column {}\nIn expectWait(predicate)\nTimed out waiting for predicate to become true", - sourceLocation.file_name(), - sourceLocation.line(), - sourceLocation.column())}; - } + /** + * @brief Deliberately skips the current test, reporting it as skipped in test results. + * @param message Justification for skipping the test. + * @param location Source location used for skip reporting (automatically captured). + */ + export [[noreturn]] inline auto skip(std::string message, std::source_location location = std::source_location::current()) -> void + { + throw TestSkipped{std::move(message), location}; + } + + /** + * @brief Conditionally skips the current test if the provided condition evaluates to true. + * @param condition Boolean flag determining whether to skip. + * @param message Justification for skipping the test. + * @param location Source location used for skip reporting (automatically captured). + */ + export inline auto skipIf(bool condition, std::string message, std::source_location location = std::source_location::current()) -> void + { + if (condition) + skip(std::move(message), location); + } + + /** + * @brief Explicitly fails the current test with a diagnostic message. + * @param message Diagnostic description explaining why the test failed. + * @param location Source location used for failure reporting (automatically captured). + */ + export [[noreturn]] inline auto fail(std::string message = "Explicit test failure", std::source_location location = std::source_location::current()) -> void + { + throw TestException{std::format( + "In {}\nAt line {}, column {}\n{}", + location.file_name(), + location.line(), + location.column(), + message)}; + } + + /** + * @brief Creates an RAII EventWatcher to observe emitted events. + * @tparam eventName Hashed string identifying the event. + * @tparam Args Payload parameter types. + * @param emitter Event emitter instance to subscribe to. + * @return EventWatcher instance managing event subscription. + */ + export template + [[nodiscard]] inline auto watch(Emitter& emitter) + { + return EventWatcher{emitter}; + } + + /** + * @brief Times the synchronous execution of a callable. + * @param name Diagnostic identifier of the benchmark. + * @param function Callable to measure. + * @return BenchmarkResult containing measured duration in nanoseconds. + */ + export inline auto benchmark(std::string name, Type::Callable auto&& function) -> BenchmarkResult + { + const auto startTime = std::chrono::steady_clock::now(); + function(); + const auto endTime = std::chrono::steady_clock::now(); + + return BenchmarkResult{ + .name = std::move(name), + .duration = std::chrono::duration_cast(endTime - startTime)}; + } + + /** + * @brief Compares two benchmark results, logs their relative speedup, and returns the comparison. + * @param first First benchmark result (candidate). + * @param second Second benchmark result (baseline). + * @return BenchmarkComparison structure with speedup and relative performance analysis. + */ + export inline auto compareBenchmarks(const BenchmarkResult& first, const BenchmarkResult& second) -> BenchmarkComparison + { + auto comparison = BenchmarkComparison{first, second}; + if (comparison.isFirstFaster()) + Logger<"UnitTests">::print<"detail">("Benchmark comparison: '{}' ({}) is {:.2f}x faster than '{}' ({})", + first.name, + first.durationString(), + comparison.speedup(), + second.name, + second.durationString()); + else + Logger<"UnitTests">::print<"detail">("Benchmark comparison: '{}' ({}) is {:.2f}x slower than '{}' ({})", + first.name, + first.durationString(), + comparison.speedup(), + second.name, + second.durationString()); + return comparison; + } - inline auto expectEqual(const auto& lhs, const auto& rhs, std::source_location sourceLocation = std::source_location::current()) -> void + /** + * @brief Emits a warning log if a candidate benchmark ran slower than a baseline benchmark. + * @param candidate Benchmark result being evaluated. + * @param baseline Reference benchmark result. + */ + export inline auto warnIfSlower(const BenchmarkResult& candidate, const BenchmarkResult& baseline) -> void + { + if (candidate.duration > baseline.duration) { - if (lhs != rhs) [[unlikely]] - throw TestException{std::format("In {}\nAt line {}, column {}\nIn expectEqual(lhs, rhs)\nwith lhs:\n{}\nand rhs:\n{}", - sourceLocation.file_name(), - sourceLocation.line(), - sourceLocation.column(), - String::formatValue(lhs), - String::formatValue(rhs))}; + const auto ratio = static_cast(candidate.duration.count()) / static_cast(baseline.duration.count()); + Logger<"UnitTests">::emit<"color">(Terminal::TextColor::TextColorEnum::Yellow, + std::format("Warning: '{}' ({}) was {:.2f}x slower than '{}' ({})", + candidate.name, + candidate.durationString(), + ratio, + baseline.name, + baseline.durationString())); } + } - template - inline auto expectThrow(auto&& function, std::source_location sourceLocation = std::source_location::current()) -> void + /** + * @brief Executes a fuzz testing loop passing a DeterministicRandomEngine to the test body. + * @param name Descriptive identifier of the fuzz test. + * @param iterations Number of iterations to execute. + * @param function Test callback receiving a DeterministicRandomEngine&. + * @param seed Initial pseudo-random number generator seed (defaults to DeterministicRandomEngine::defaultSeed). + * @param location Source location used for failure reporting (automatically captured). + */ + export inline auto fuzz( + std::string name, + std::size_t iterations, + Type::Callable auto&& function, + std::uint64_t seed = Math::DeterministicRandomEngine::defaultSeed, + std::source_location location = std::source_location::current()) -> void + { + auto fuzzer = Math::DeterministicRandomEngine{seed}; + for (auto iteration : std::views::iota(0uz, iterations)) { try { - function(); - throw TestException{std::format("In {}\nAt line {}, column {}\nExpected throw of type {}, but no exception was thrown.", - sourceLocation.file_name(), - sourceLocation.line(), - sourceLocation.column(), - typeid(ExceptionType).name())}; + function(fuzzer); } - catch (const ExceptionType&) + catch (const TestException& exception) { - Logger::emit<"success">("Expected exception caught."); + throw TestException{std::format("In {}\nAt line {}, column {}\nFuzz test '{}' failed at iteration {} with seed {:#x}:\n{}", + location.file_name(), + location.line(), + location.column(), + name, + iteration, + seed, + exception.what())}; } catch (const std::exception& exception) { - throw TestException{std::format("In {}\nAt line {}, column {}\nExpected throw of type {}, but caught different exception: {}", - sourceLocation.file_name(), - sourceLocation.line(), - sourceLocation.column(), - typeid(ExceptionType).name(), + throw TestException{std::format("In {}\nAt line {}, column {}\nFuzz test '{}' threw exception at iteration {} with seed {:#x}: {}", + location.file_name(), + location.line(), + location.column(), + name, + iteration, + seed, exception.what())}; } } + } - inline auto expectNoThrow(auto&& function, std::source_location sourceLocation = std::source_location::current()) -> void + /** + * @brief Runs all registered test suites respecting dependency order and configuration settings. + * @param settings Execution settings controlling verbose output, filtering, and abort conditions. + * @return Exit code: 0 on success, non-zero if any test suite failed. + */ + export inline auto executeTests(TestSettings settings) -> int + { + return testRunner.executeTests(std::move(settings)); + } + + /** + * @brief Parses command-line arguments into a TestSettings configuration object. + * @param arguments Span of command-line argument tokens. + * @return Parsed TestSettings configuration. + */ + export inline auto executeCommands(std::span arguments) -> TestSettings + { + auto settings = TestSettings{}; + const auto parametersLexer = Language::Parameters::ParametersLexer{}; + const auto parameters = parametersLexer.parseParameters(arguments); + for (const auto& [parameterName, parameterValue] : parameters) { - try + if (parameterName == "help" or parameterName == "h") { - function(); + Logger<"UnitTests">::print<"info">( + "Available parameters:\n" + " -h, --help Show this help message\n" + " -m, --mode= Execution mode [debug|test|prod]\n" + " -v, --verbose[=] Enable/disable verbose test output\n" + " -d, --detail[=] Enable/disable execution details\n" + " --fail-fast Stop on first failure\n" + " -c, --chrono[=] Display test execution times\n" + " -f, --filter= Filter test suites by substring\n" + " -r, --report= Export test results (e.g. junit:report.xml)\n\n" + "Usage: ./UnitTests --filter=Log --verbose --report=junit:report.xml"); + settings.abort = true; + return settings; } - catch (const std::exception& exception) + else if (parameterName == "mode" or parameterName == "m") { - throw TestException{std::format("In {}\nAt line {}, column {}\nExpected no throw, but an exception was thrown: {}", - sourceLocation.file_name(), - sourceLocation.line(), - sourceLocation.column(), - exception.what())}; + if (parameterValue == "test") + settings.chrono = true; + else if (parameterValue == "prod") + { + settings.verbose = false; + settings.detail = false; + settings.failFast = true; + } } + else if (parameterName == "verbose" or parameterName == "v") + settings.verbose = (std::ranges::empty(parameterValue) or parameterValue == "1" or parameterValue == "true"); + else if (parameterName == "detail" or parameterName == "d") + settings.detail = (std::ranges::empty(parameterValue) or parameterValue == "1" or parameterValue == "true"); + else if (parameterName == "fail-fast") + settings.failFast = (std::ranges::empty(parameterValue) or parameterValue == "1" or parameterValue == "true"); + else if (parameterName == "chrono" or parameterName == "c") + settings.chrono = (std::ranges::empty(parameterValue) or parameterValue == "1" or parameterValue == "true"); + else if (parameterName == "filter" or parameterName == "f") + settings.filter = parameterValue; + else if (parameterName == "report" or parameterName == "r") + settings.report = parameterValue; + else + Logger<"CppUtils">::print<"warning">("Unknown command: {}", parameterName); } + return settings; + } - std::vector tests; - }; - - export inline auto executeTests(TestSettings settings) -> int + /** + * @brief Parses standard main() arguments (argc, argv) into a TestSettings configuration object. + * @param argc Argument count. + * @param argv Array of argument strings. + * @return Parsed TestSettings configuration. + */ + export inline auto executeCommands(int argc, const char* const argv[]) -> TestSettings { - return testRunner.executeTests(std::move(settings)); + if (argc <= 1 or argv == nullptr) + return TestSettings{}; + return executeCommands(std::vector{argv + 1, argv + argc}); } } diff --git a/tests/Chrono/Chronometer.mpp b/tests/Chrono/Chronometer.mpp index 0a0297f3..fdd42c22 100644 --- a/tests/Chrono/Chronometer.mpp +++ b/tests/Chrono/Chronometer.mpp @@ -5,15 +5,15 @@ import CppUtils; namespace CppUtils::UnitTest::Chrono::Chronometer { - auto _ = TestSuite{"Chrono", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Chrono").dependsOn("Logger") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("Chronometer", [&] { + suite.test("Chronometer") = [&] { auto chrono = CppUtils::Chrono::Chronometer{}; chrono.stop(); auto string = CppUtils::Chrono::durationToString(chrono.elapsed()); - suite.expect(not std::ranges::empty(string)); + expect(not std::ranges::empty(string)); Logger::print("{}", string); - }); - }}; + }; + }; } diff --git a/tests/Container/BTree.mpp b/tests/Container/BTree.mpp index 62d4713d..c52f6abf 100644 --- a/tests/Container/BTree.mpp +++ b/tests/Container/BTree.mpp @@ -5,118 +5,118 @@ import CppUtils; namespace CppUtils::UnitTest::Container::BTree { - auto _ = TestSuite{"Container/BTree", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Container/BTree").dependsOn("Logger") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("Empty", [&] { + suite.test("Empty") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; Logger::print("{}", bTree); - suite.expect(std::ranges::empty(bTree)); - }); + expect(std::ranges::empty(bTree)); + }; - suite.addTest("1 element", [&] { + suite.test("1 element") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; bTree.add(0, 'A'); Logger::print("{}", bTree); - suite.expect(not std::ranges::empty(bTree)); - suite.expectEqual(std::ranges::size(bTree), 1uz); - }); + expect(not std::ranges::empty(bTree)); + expect(std::ranges::size(bTree)) == 1uz; + }; - suite.addTest("2 elements", [&] { + suite.test("2 elements") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; bTree.add(0, 'A'); bTree.add(1, 'B'); Logger::print("{}", bTree); - suite.expectEqual(std::ranges::size(bTree), 2uz); - }); + expect(std::ranges::size(bTree)) == 2uz; + }; - suite.addTest("5 elements (split self)", [&] { + suite.test("5 elements (split self)") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; for (auto i = 0; i < 5; ++i) bTree.add(i, 'A' + static_cast(i)); Logger::print("{}", bTree); - suite.expectEqual(std::ranges::size(bTree), 5uz); - }); + expect(std::ranges::size(bTree)) == 5uz; + }; - suite.addTest("8 elements (split self & split child)", [&] { + suite.test("8 elements (split self & split child)") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; for (auto i = 0; i < 8; ++i) bTree.add(i, 'A' + static_cast(i)); Logger::print("{}", bTree); - suite.expectEqual(std::ranges::size(bTree), 8uz); - }); + expect(std::ranges::size(bTree)) == 8uz; + }; - suite.addTest("10 elements", [&] { + suite.test("10 elements") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; for (auto i = 0; i < 10; ++i) bTree.add(i, 'A' + static_cast(i)); Logger::print("{}", bTree); - suite.expectEqual(std::ranges::size(bTree), 10uz); - }); + expect(std::ranges::size(bTree)) == 10uz; + }; - suite.addTest("Exists", [&] { + suite.test("Exists") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; bTree.add(0, 'A'); Logger::print("{}", bTree); - suite.expect(bTree.exists(0)); - suite.expect(not bTree.exists(1)); - }); + expect(bTree.exists(0)); + expect(not bTree.exists(1)); + }; - suite.addTest("Key hasher", [&] { + suite.test("Key hasher") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; bTree.add("test", 0); Logger::print("{}", bTree); - suite.expectEqual(std::ranges::size(bTree), 1uz); - }); + expect(std::ranges::size(bTree)) == 1uz; + }; - suite.addTest("Removing element", [&] { + suite.test("Removing element") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; bTree.add(0, 'A'); bTree.remove(0); Logger::print("{}", bTree); - suite.expect(std::ranges::empty(bTree)); - }); + expect(std::ranges::empty(bTree)); + }; - suite.addTest("Removing nested element", [&] { + suite.test("Removing nested element") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; Logger::print("{}", bTree); - suite.expect(std::ranges::empty(bTree)); - }); + expect(std::ranges::empty(bTree)); + }; - suite.addTest("Clear", [&] { + suite.test("Clear") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; bTree.add(0, 'A'); bTree.clear(); Logger::print("{}", bTree); - suite.expect(std::ranges::empty(bTree)); - }); + expect(std::ranges::empty(bTree)); + }; - suite.addTest("Search key", [&] { + suite.test("Search key") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; bTree.add(0, 'A'); Logger::print("{}", bTree); - suite.expect(bTree.exists(0)); - suite.expectEqual(bTree.get(0), 'A'); - }); + expect(bTree.exists(0)); + expect(bTree.get(0)) == 'A'; + }; - suite.addTest("Set value", [&] { + suite.test("Set value") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; bTree[0] = 'A'; Logger::print("{}", bTree); - suite.expect(not std::ranges::empty(bTree)); - suite.expect(bTree.exists(0)); - suite.expectEqual(bTree.get(0), 'A'); - }); + expect(not std::ranges::empty(bTree)); + expect(bTree.exists(0)); + expect(bTree.get(0)) == 'A'; + }; - suite.addTest("Edit value", [&] { + suite.test("Edit value") = [&] { auto bTree = CppUtils::Container::BTree::Node{}; bTree.add(0, 'A'); bTree[0] = 'B'; Logger::print("{}", bTree); - suite.expectEqual(bTree.size(), 1uz); - suite.expect(bTree.exists(0)); - suite.expectEqual(bTree.get(0), 'B'); - }); - }}; + expect(bTree.size()) == 1uz; + expect(bTree.exists(0)); + expect(bTree.get(0)) == 'B'; + }; + }; } diff --git a/tests/Container/BidirectionalMap.mpp b/tests/Container/BidirectionalMap.mpp index 9d636d0c..8180b6aa 100644 --- a/tests/Container/BidirectionalMap.mpp +++ b/tests/Container/BidirectionalMap.mpp @@ -5,87 +5,85 @@ import CppUtils; namespace CppUtils::UnitTest::Container::BidirectionalMap { - auto _ = TestSuite{"Container/BidirectionalMap", {"UnitTest"}, [](TestSuite& suite) { + auto _ = testSuite("Container/BidirectionalMap").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("Empty Map", [&] { + suite.test("Empty Map") = [&] { auto map = CppUtils::Container::BidirectionalMap{}; - suite.expect(std::ranges::empty(map)); - suite.expectEqual(std::ranges::size(map), 0uz); - }); + expect(std::ranges::empty(map)); + expect(std::ranges::size(map)) == 0uz; + }; - suite.addTest("One value", [&] { + suite.test("One value") = [&] { auto map = CppUtils::Container::BidirectionalMap{}; map.insert({42, "Hello World!"s}); - suite.expect(not std::ranges::empty(map)); - suite.expectEqual(std::ranges::size(map), 1uz); - suite.expectEqual(map.left(42), "Hello World!"sv); - suite.expectEqual(map.right("Hello World!"s), 42); - }); + expect(not std::ranges::empty(map)); + expect(std::ranges::size(map)) == 1uz; + expect(map.left(42)) == "Hello World!"; + expect(map.right("Hello World!"s)) == 42; + }; - suite.addTest("Erase value", [&] { + suite.test("Erase value") = [&] { auto map = CppUtils::Container::BidirectionalMap{}; map.insert({42, "Hello World!"s}); map.eraseLeft(42); - suite.expect(std::ranges::empty(map)); - }); + expect(std::ranges::empty(map)); + }; - suite.addTest("Direct insert with two arguments", [&] { + suite.test("Direct insert with two arguments") = [&] { auto map = CppUtils::Container::BidirectionalMap{}; - suite.expect(map.insert(1, "one"s)); - suite.expect(map.insert(2, "two"s)); - suite.expect(not map.insert(1, "another one"s)); // Collision on left - suite.expect(not map.insert(3, "two"s)); // Collision on right - suite.expectEqual(std::ranges::size(map), 2uz); - suite.expect(map.containsLeft(1)); - suite.expect(map.containsRight("two"s)); - suite.expect(not map.containsLeft(99)); - suite.expect(not map.containsRight("unknown"s)); - }); + expect(map.insert(1, "one"s)); + expect(map.insert(2, "two"s)); + expect(not map.insert(1, "another one"s)); // Collision on left + expect(not map.insert(3, "two"s)); // Collision on right + expect(std::ranges::size(map)) == 2uz; + expect(map.containsLeft(1)); + expect(map.containsRight("two"s)); + expect(not map.containsLeft(99)); + expect(not map.containsRight("unknown"s)); + }; - suite.addTest("Variadic construction", [&] { + suite.test("Variadic construction") = [&] { using BidiMap = CppUtils::Container::BidirectionalMap; auto map = BidiMap{ std::pair{1, "un"s}, std::pair{2, "deux"s}, - std::pair{3, "trois"s} - }; - suite.expectEqual(std::ranges::size(map), 3uz); - suite.expectEqual(map.left(1), "un"s); - suite.expectEqual(map.right("deux"s), 2); - }); + std::pair{3, "trois"s}}; + expect(std::ranges::size(map)) == 3uz; + expect(map.left(1)) == "un"; + expect(map.right("deux"s)) == 2; + }; - suite.addTest("Iterator constructor", [&] { + suite.test("Iterator constructor") = [&] { using Pair = std::pair; const auto source = std::vector{ {10, "ten"s}, - {20, "twenty"s} - }; + {20, "twenty"s}}; const auto map = CppUtils::Container::BidirectionalMap{std::ranges::begin(source), std::ranges::end(source)}; - suite.expectEqual(std::ranges::size(map), 2uz); - suite.expectEqual(map.left(10), "ten"s); - suite.expectEqual(map.right("twenty"s), 20); - }); + expect(std::ranges::size(map)) == 2uz; + expect(map.left(10)) == "ten"; + expect(map.right("twenty"s)) == 20; + }; - suite.addTest("Erase right", [&] { + suite.test("Erase right") = [&] { auto map = CppUtils::Container::BidirectionalMap{}; map.insert(10, "ten"s); map.insert(20, "twenty"s); - suite.expect(map.eraseRight("ten"s)); - suite.expectEqual(std::ranges::size(map), 1uz); - suite.expect(not map.containsLeft(10)); - suite.expect(map.containsLeft(20)); - suite.expect(not map.eraseRight("unknown"s)); - }); + expect(map.eraseRight("ten"s)); + expect(std::ranges::size(map)) == 1uz; + expect(not map.containsLeft(10)); + expect(map.containsLeft(20)); + expect(not map.eraseRight("unknown"s)); + }; - suite.addTest("Clear", [&] { + suite.test("Clear") = [&] { auto map = CppUtils::Container::BidirectionalMap{}; map.insert(1, "one"s); map.insert(2, "two"s); - suite.expect(not std::ranges::empty(map)); + expect(not std::ranges::empty(map)); map.clear(); - suite.expect(std::ranges::empty(map)); - suite.expectEqual(std::ranges::size(map), 0uz); - }); - }}; + expect(std::ranges::empty(map)); + expect(std::ranges::size(map)) == 0uz; + }; + }; } diff --git a/tests/Container/DependencyGraph.mpp b/tests/Container/DependencyGraph.mpp index 784ef9d9..b73b0164 100644 --- a/tests/Container/DependencyGraph.mpp +++ b/tests/Container/DependencyGraph.mpp @@ -5,33 +5,33 @@ import CppUtils; namespace CppUtils::UnitTest::Container::DependencyGraph { - auto _ = TestSuite{"Container/DependencyGraph", {"UnitTest", "Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Container/DependencyGraph").dependsOn("UnitTest", "Logger") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("Empty graph", [&] { + suite.test("Empty graph") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; - suite.expect(not graph.hasCycle()); - }); + expect(not graph.hasCycle()); + }; - suite.addTest("One element", [&] { + suite.test("One element") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); - suite.expect(graph.contains('A')); - suite.expectEqual(graph['A'], 1); - }); + expect(graph.contains('A')); + expect(graph['A']) == 1; + }; - suite.addTest("Remove element", [&] { + suite.test("Remove element") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph.remove('A'); - suite.expect(not graph.contains('A')); - }); + expect(not graph.contains('A')); + }; - suite.addTest("Remove element with dependencies", [&] { + suite.test("Remove element with dependencies") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph.insert('B', 2); @@ -39,21 +39,21 @@ namespace CppUtils::UnitTest::Container::DependencyGraph graph.addDependency('B', 'A'); graph.remove('B'); - suite.expect(not graph.contains('B')); - suite.expect(graph.contains('A')); + expect(not graph.contains('B')); + expect(graph.contains('A')); const auto& aDeps = graph.getDependencies('A'); - suite.expect(std::ranges::empty(aDeps)); - }); + expect(std::ranges::empty(aDeps)); + }; - suite.addTest("Change value", [&] { + suite.test("Change value") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph['A'] = 2; - suite.expectEqual(graph['A'], 2); - }); + expect(graph['A']) == 2; + }; - suite.addTest("Add dependency", [&] { + suite.test("Add dependency") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph.insert('B', 2); @@ -62,14 +62,14 @@ namespace CppUtils::UnitTest::Container::DependencyGraph const auto& aDeps = graph.getDependencies('A'); const auto& bDeps = graph.getDependencies('B'); - suite.expectEqual(std::ranges::size(aDeps), 1uz); - suite.expectEqual(aDeps[0], 'B'); - suite.expect(std::ranges::empty(bDeps)); + expect(std::ranges::size(aDeps)) == 1uz; + expect(aDeps[0]) == 'B'; + expect(std::ranges::empty(bDeps)); - suite.expect(not graph.hasCycle()); - }); + expect(not graph.hasCycle()); + }; - suite.addTest("Remove dependency", [&] { + suite.test("Remove dependency") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph.insert('B', 2); @@ -79,13 +79,13 @@ namespace CppUtils::UnitTest::Container::DependencyGraph const auto& aDeps = graph.getDependencies('A'); const auto& bDeps = graph.getDependencies('B'); - suite.expect(std::ranges::empty(aDeps)); - suite.expect(std::ranges::empty(bDeps)); + expect(std::ranges::empty(aDeps)); + expect(std::ranges::empty(bDeps)); - suite.expect(not graph.hasCycle()); - }); + expect(not graph.hasCycle()); + }; - suite.addTest("Circular dependency (direct)", [&] { + suite.test("Circular dependency (direct)") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph.insert('B', 2); @@ -95,15 +95,15 @@ namespace CppUtils::UnitTest::Container::DependencyGraph const auto& aDeps = graph.getDependencies('A'); const auto& bDeps = graph.getDependencies('B'); - suite.expectEqual(std::ranges::size(aDeps), 1uz); - suite.expectEqual(std::ranges::size(bDeps), 1uz); - suite.expectEqual(aDeps[0], 'B'); - suite.expectEqual(bDeps[0], 'A'); + expect(std::ranges::size(aDeps)) == 1uz; + expect(std::ranges::size(bDeps)) == 1uz; + expect(aDeps[0]) == 'B'; + expect(bDeps[0]) == 'A'; - suite.expect(graph.hasCycle()); - }); + expect(graph.hasCycle()); + }; - suite.addTest("Circular dependency (indirect)", [&] { + suite.test("Circular dependency (indirect)") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph.insert('B', 2); @@ -119,19 +119,19 @@ namespace CppUtils::UnitTest::Container::DependencyGraph const auto& cDeps = graph.getDependencies('C'); const auto& dDeps = graph.getDependencies('D'); - suite.expectEqual(std::ranges::size(aDeps), 1uz); - suite.expectEqual(std::ranges::size(bDeps), 1uz); - suite.expectEqual(std::ranges::size(cDeps), 1uz); - suite.expectEqual(std::ranges::size(dDeps), 1uz); - suite.expectEqual(aDeps[0], 'B'); - suite.expectEqual(bDeps[0], 'C'); - suite.expectEqual(cDeps[0], 'D'); - suite.expectEqual(dDeps[0], 'B'); + expect(std::ranges::size(aDeps)) == 1uz; + expect(std::ranges::size(bDeps)) == 1uz; + expect(std::ranges::size(cDeps)) == 1uz; + expect(std::ranges::size(dDeps)) == 1uz; + expect(aDeps[0]) == 'B'; + expect(bDeps[0]) == 'C'; + expect(cDeps[0]) == 'D'; + expect(dDeps[0]) == 'B'; - suite.expect(graph.hasCycle()); - }); + expect(graph.hasCycle()); + }; - suite.addTest("ForEach (without circular dependencies)", [&] { + suite.test("ForEach (without circular dependencies)") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph.insert('B', 2); @@ -140,14 +140,14 @@ namespace CppUtils::UnitTest::Container::DependencyGraph graph.addDependency('A', 'C'); graph.addDependency('B', 'C'); - suite.expect(not graph.hasCycle()); + expect(not graph.hasCycle()); - suite.expect(graph.forEach([](const auto& key, const auto& value, [[maybe_unused]] const auto& dependencies) -> void { + expect(graph.forEach([](const auto& key, const auto& value, [[maybe_unused]] const auto& dependencies) -> void { Logger::print("{}: {}", key, value); }).has_value()); - }); + }; - suite.addTest("ForEach (with circular dependencies)", [&] { + suite.test("ForEach (with circular dependencies)") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph.insert('B', 2); @@ -158,16 +158,16 @@ namespace CppUtils::UnitTest::Container::DependencyGraph graph.addDependency('C', 'D'); graph.addDependency('D', 'B'); - suite.expect(graph.hasCycle()); + expect(graph.hasCycle()); auto result = graph.forEach([](const auto& key, const auto& value, [[maybe_unused]] const auto& dependencies) -> void { Logger::print("{}: {}", key, value); }); - suite.expect(not result); + expect(not result); Logger::print("Error: {}", result.error()); - }); + }; - suite.addTest("ForEach (interruption)", [&] { + suite.test("ForEach (interruption)") = [&] { auto graph = CppUtils::Container::DependencyGraph{}; graph.insert('A', 1); graph.insert('B', 2); @@ -180,9 +180,9 @@ namespace CppUtils::UnitTest::Container::DependencyGraph return false; }); - suite.expect(result); - suite.expect(not *result); - suite.expectEqual(count, 1uz); - }); - }}; + expect(result); + expect(not *result); + expect(count) == 1uz; + }; + }; } diff --git a/tests/Container/FlatHashMap.mpp b/tests/Container/FlatHashMap.mpp index 1c15e9f8..0a78dd62 100644 --- a/tests/Container/FlatHashMap.mpp +++ b/tests/Container/FlatHashMap.mpp @@ -5,142 +5,142 @@ import CppUtils; namespace CppUtils::UnitTest::Container::FlatHashMap { - static_assert([] { - auto map = CppUtils::Container::FlatHashMap{}; - map[1] = 100; - map[2] = 200; - map[3] = 300; - if (std::ranges::size(map) != 3uz) - return false; - if (map.at(1) != 100) - return false; - if (not map.contains(2)) - return false; - if (map.contains(4)) - return false; - map.erase(2); - if (map.contains(2)) - return false; - return std::ranges::size(map) == 2uz; - }()); - - static_assert([] { - auto map = CppUtils::Container::FlatHashMap{}; - auto [iteratorFirst, insertedFirst] = map.try_emplace(1, 10); - if (not insertedFirst or iteratorFirst->second != 10) - return false; - - auto [iteratorSecond, insertedSecond] = map.try_emplace(1, 99); - if (insertedSecond or iteratorSecond->second != 10) - return false; - - auto [iteratorThird, assignedFirst] = map.insert_or_assign(1, 50); - if (assignedFirst or iteratorThird->second != 50) - return false; - - auto [iteratorFourth, assignedSecond] = map.insert_or_assign(2, 60); - if (not assignedSecond or iteratorFourth->second != 60) - return false; - - return std::ranges::size(map) == 2uz; - }()); - - static_assert([] { - auto map = CppUtils::Container::FlatHashMap{{1, 10}, {2, 20}, {3, 30}}; - auto sum = 0; - for (const auto& [key, value] : map) - sum += key + value; - return sum == (1 + 10 + 2 + 20 + 3 + 30); - }()); - - static_assert([] { + auto _ = testSuite("Container/FlatHashMap").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace String::Literals; - auto tokenMap = CppUtils::Container::FlatHashMap{}; - tokenMap["foo"_token] = 42; - tokenMap["bar"_token] = 84; - if (tokenMap["foo"_token] != 42 or tokenMap["bar"_token] != 84) - return false; - if (not tokenMap.contains("foo"_token)) - return false; - tokenMap.erase("foo"_token); - if (tokenMap.contains("foo"_token)) - return false; - - return true; - }()); - - auto _ = TestSuite{"Container/FlatHashMap", {"UnitTest"}, [](TestSuite& suite) { - using namespace String::Literals; + suite.test<[] { + auto map = CppUtils::Container::FlatHashMap{}; + map[1] = 100; + map[2] = 200; + map[3] = 300; + if (std::ranges::size(map) != 3uz) + return false; + if (map.at(1) != 100) + return false; + if (not map.contains(2)) + return false; + if (map.contains(4)) + return false; + map.erase(2); + if (map.contains(2)) + return false; + return std::ranges::size(map) == 2uz; + }>("Constexpr operator[] and erase"); + + suite.test<[] { + auto map = CppUtils::Container::FlatHashMap{}; + auto [iteratorFirst, insertedFirst] = map.try_emplace(1, 10); + if (not insertedFirst or iteratorFirst->second != 10) + return false; + + auto [iteratorSecond, insertedSecond] = map.try_emplace(1, 99); + if (insertedSecond or iteratorSecond->second != 10) + return false; + + auto [iteratorThird, assignedFirst] = map.insert_or_assign(1, 50); + if (assignedFirst or iteratorThird->second != 50) + return false; + + auto [iteratorFourth, assignedSecond] = map.insert_or_assign(2, 60); + if (not assignedSecond or iteratorFourth->second != 60) + return false; + + return std::ranges::size(map) == 2uz; + }>("Constexpr emplace and assign"); - suite.addTest("Basic insertion and lookup", [&] { + suite.test<[] { + auto map = CppUtils::Container::FlatHashMap{{1, 10}, {2, 20}, {3, 30}}; + auto sum = 0; + for (const auto& [key, value] : map) + sum += key + value; + return sum == (1 + 10 + 2 + 20 + 3 + 30); + }>("Constexpr initializer list and iteration"); + + suite.test<[] { + using namespace String::Literals; + + auto tokenMap = CppUtils::Container::FlatHashMap{}; + tokenMap["foo"_token] = 42; + tokenMap["bar"_token] = 84; + if (tokenMap["foo"_token] != 42 or tokenMap["bar"_token] != 84) + return false; + if (not tokenMap.contains("foo"_token)) + return false; + tokenMap.erase("foo"_token); + if (tokenMap.contains("foo"_token)) + return false; + + return true; + }>("Constexpr token keys"); + + suite.test("Basic insertion and lookup") = [&] { auto map = CppUtils::Container::FlatHashMap{}; - suite.expect(std::ranges::empty(map)); - suite.expectEqual(std::ranges::size(map), 0uz); + expect(std::ranges::empty(map)); + expect(std::ranges::size(map)) == 0uz; map["alpha"] = 1; map["beta"] = 2; map["gamma"] = 3; - suite.expect(not std::ranges::empty(map)); - suite.expectEqual(std::ranges::size(map), 3uz); - suite.expect(map.contains("alpha")); - suite.expect(map.contains("beta")); - suite.expect(map.contains("gamma")); - suite.expect(not map.contains("delta")); + expect(not std::ranges::empty(map)); + expect(std::ranges::size(map)) == 3uz; + expect(map.contains("alpha")); + expect(map.contains("beta")); + expect(map.contains("gamma")); + expect(not map.contains("delta")); - suite.expectEqual(map.at("alpha"), 1); - suite.expectEqual(map.at("beta"), 2); - suite.expectEqual(map.at("gamma"), 3); - }); + expect(map.at("alpha")) == 1; + expect(map.at("beta")) == 2; + expect(map.at("gamma")) == 3; + }; - suite.addTest("Operator[] modification and insertion", [&] { + suite.test("Operator[] modification and insertion") = [&] { auto map = CppUtils::Container::FlatHashMap{}; map[10] = "ten"; - suite.expectEqual(map[10], "ten"); + expect(map[10]) == "ten"; map[10] = "dix"; - suite.expectEqual(map[10], "dix"); - suite.expectEqual(std::ranges::size(map), 1uz); - }); + expect(map[10]) == "dix"; + expect(std::ranges::size(map)) == 1uz; + }; - suite.addTest("Erase and tombstone reuse", [&] { + suite.test("Erase and tombstone reuse") = [&] { auto map = CppUtils::Container::FlatHashMap{}; for (auto index = 0uz; index < 20uz; ++index) map[index] = index * 10uz; - suite.expectEqual(std::ranges::size(map), 20uz); + expect(std::ranges::size(map)) == 20uz; for (auto index = 0uz; index < 10uz; ++index) - suite.expectEqual(map.erase(index), 1uz); + expect(map.erase(index)) == 1uz; - suite.expectEqual(std::ranges::size(map), 10uz); + expect(std::ranges::size(map)) == 10uz; for (auto index = 0uz; index < 10uz; ++index) - suite.expect(not map.contains(index)); + expect(not map.contains(index)); for (auto index = 10uz; index < 20uz; ++index) - suite.expect(map.contains(index)); + expect(map.contains(index)); for (auto index = 0uz; index < 10uz; ++index) map[index] = index * 100uz; - suite.expectEqual(std::ranges::size(map), 20uz); + expect(std::ranges::size(map)) == 20uz; for (auto index = 0uz; index < 10uz; ++index) - suite.expectEqual(map.at(index), index * 100uz); - }); + expect(map.at(index)) == index * 100; + }; - suite.addTest("Growth and rehash", [&] { + suite.test("Growth and rehash") = [&] { auto map = CppUtils::Container::FlatHashMap{}; constexpr auto count = 1000uz; for (auto index = 0uz; index < count; ++index) map[index] = index * 2uz; - suite.expectEqual(std::ranges::size(map), count); + expect(std::ranges::size(map)) == count; for (auto index = 0uz; index < count; ++index) - suite.expectEqual(map.at(index), index * 2uz); - }); + expect(map.at(index)) == index * 2; + }; - suite.addTest("Range-based for iteration", [&] { + suite.test("Range-based for iteration") = [&] { auto map = CppUtils::Container::FlatHashMap{}; map[1] = 10; map[2] = 20; @@ -153,70 +153,101 @@ namespace CppUtils::UnitTest::Container::FlatHashMap keySum += key; valueSum += value; } - suite.expectEqual(keySum, 6); - suite.expectEqual(valueSum, 60); - }); + expect(keySum) == 6; + expect(valueSum) == 60; + }; - suite.addTest("Token keys", [&] { + suite.test("Token keys") = [&] { auto map = CppUtils::Container::FlatHashMap{}; map["first"_token] = "one"; map["second"_token] = "two"; map["third"_token] = "three"; - suite.expect(map.contains("first"_token)); - suite.expect(map.contains("second"_token)); - suite.expect(map.contains("third"_token)); - suite.expect(not map.contains("fourth"_token)); + expect(map.contains("first"_token)); + expect(map.contains("second"_token)); + expect(map.contains("third"_token)); + expect(not map.contains("fourth"_token)); - suite.expectEqual(map["first"_token], "one"); - suite.expectEqual(map["second"_token], "two"); - suite.expectEqual(map["third"_token], "three"); - }); + expect(map["first"_token]) == "one"; + expect(map["second"_token]) == "two"; + expect(map["third"_token]) == "three"; + }; - suite.addTest("Equality operator", [&] { + suite.test("Equality operator") = [&] { auto map1 = CppUtils::Container::FlatHashMap{{1, 10}, {2, 20}}; auto map2 = CppUtils::Container::FlatHashMap{{2, 20}, {1, 10}}; auto map3 = CppUtils::Container::FlatHashMap{{1, 10}, {2, 25}}; - suite.expect(map1 == map2); - suite.expect(not(map1 == map3)); - }); + expect(map1) == map2; + expect(not(map1 == map3)); + }; - suite.addTest("Runtime performance benchmark vs std::unordered_map", [&] { + suite.test("Runtime performance benchmark vs std::unordered_map") = [] { constexpr auto elementCount = 50'000uz; auto flatMap = CppUtils::Container::FlatHashMap{}; - const auto startFlatInsert = std::chrono::high_resolution_clock::now(); - for (auto index = 0uz; index < elementCount; ++index) - flatMap[index] = index * 2uz; - const auto flatInsertDuration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - startFlatInsert).count(); - - auto flatReadSum = 0uz; - const auto startFlatRead = std::chrono::high_resolution_clock::now(); - for (auto index = 0uz; index < elementCount; ++index) - flatReadSum += flatMap.at(index); - const auto flatReadDuration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - startFlatRead).count(); - auto unorderedMap = std::unordered_map{}; - const auto startUnorderedInsert = std::chrono::high_resolution_clock::now(); - for (auto index = 0uz; index < elementCount; ++index) - unorderedMap[index] = index * 2uz; - const auto unorderedInsertDuration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - startUnorderedInsert).count(); - auto unorderedReadSum = 0uz; - const auto startUnorderedRead = std::chrono::high_resolution_clock::now(); - for (auto index = 0uz; index < elementCount; ++index) - unorderedReadSum += unorderedMap.at(index); - const auto unorderedReadDuration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - startUnorderedRead).count(); + const auto flatInsert = benchmark("FlatHashMap insert", [&] { + for (auto index = 0uz; index < elementCount; ++index) + flatMap[index] = index * 2uz; + }); - suite.expectEqual(flatReadSum, unorderedReadSum); - suite.expectEqual(std::ranges::size(flatMap), std::ranges::size(unorderedMap)); + const auto unorderedInsert = benchmark("std::unordered_map insert", [&] { + for (auto index = 0uz; index < elementCount; ++index) + unorderedMap[index] = index * 2uz; + }); - if (flatReadDuration > unorderedReadDuration) - std::println("Warning: FlatHashMap read ({} µs) was slower than std::unordered_map ({} µs)", flatReadDuration, unorderedReadDuration); + auto flatReadSum = 0uz; + const auto flatRead = benchmark("FlatHashMap read", [&] { + for (auto index = 0uz; index < elementCount; ++index) + flatReadSum += flatMap.at(index); + }); - if (flatInsertDuration > unorderedInsertDuration) - std::println("Warning: FlatHashMap insert ({} µs) was slower than std::unordered_map ({} µs)", flatInsertDuration, unorderedInsertDuration); - }); - }}; + auto unorderedReadSum = 0uz; + const auto unorderedRead = benchmark("std::unordered_map read", [&] { + for (auto index = 0uz; index < elementCount; ++index) + unorderedReadSum += unorderedMap.at(index); + }); + + expect(flatReadSum) == unorderedReadSum; + expect(std::ranges::size(flatMap)) == std::ranges::size(unorderedMap); + + compareBenchmarks(flatInsert, unorderedInsert); + compareBenchmarks(flatRead, unorderedRead); + + warnIfSlower(flatInsert, unorderedInsert); + warnIfSlower(flatRead, unorderedRead); + }; + + suite.test("Differential fuzzing vs std::unordered_map") = [] { + auto flatMap = CppUtils::Container::FlatHashMap{}; + auto referenceMap = std::unordered_map{}; + + fuzz("Random operations on FlatHashMap", 2'000uz, [&](auto& fuzzer) { + const auto operation = fuzzer.random(0uz, 2uz); + const auto key = fuzzer.random(-100, 100); + const auto randomValue = fuzzer.random(-1'000, 1'000); + + switch (operation) + { + case 0: + flatMap[key] = randomValue; + referenceMap[key] = randomValue; + break; + case 1: + flatMap.erase(key); + referenceMap.erase(key); + break; + case 2: + expect(flatMap.contains(key)) == referenceMap.contains(key); + if (referenceMap.contains(key)) + expect(flatMap.at(key)) == referenceMap.at(key); + break; + } + + expect(std::ranges::size(flatMap)) == std::ranges::size(referenceMap); + }); + }; + }; } diff --git a/tests/Container/MeshNetwork.mpp b/tests/Container/MeshNetwork.mpp index 2671b247..8d057a16 100644 --- a/tests/Container/MeshNetwork.mpp +++ b/tests/Container/MeshNetwork.mpp @@ -5,24 +5,24 @@ import CppUtils; namespace CppUtils::UnitTest::Container::MeshNetwork { - auto _ = TestSuite{"Container/MeshNetwork", {"Container/NetworkPtr"}, [](TestSuite& suite) { + auto _ = testSuite("Container/MeshNetwork").dependsOn("Container/NetworkPtr") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; using StringMeshNodePtr = CppUtils::Container::MeshNodePtr; - suite.addTest("Initialization", [&] { + suite.test("Initialization") = [&] { auto _ = StringMeshNodePtr::make("banana"); - }); + }; - suite.addTest("Access value", [&] { + suite.test("Access value") = [&] { auto node = StringMeshNodePtr::make("apple"); auto accessor = node.getValue(); const auto& value = accessor.value(); Logger::print("{}", value); - suite.expectEqual(value, "apple"); - }); + expect(value) == "apple"; + }; - suite.addTest("Copy reference", [&] { + suite.test("Copy reference") = [&] { auto node2 = StringMeshNodePtr{}; { auto node1 = StringMeshNodePtr::make("apple"); @@ -31,10 +31,10 @@ namespace CppUtils::UnitTest::Container::MeshNetwork auto accessor = node2.getValue(); const auto& value = accessor.value(); Logger::print("{}", value); - suite.expectEqual(value, "apple"); - }); + expect(value) == "apple"; + }; - suite.addTest("Attach child", [&] { + suite.test("Attach child") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); { auto apple = StringMeshNodePtr::make("apple"); @@ -43,45 +43,45 @@ namespace CppUtils::UnitTest::Container::MeshNetwork auto fruitAccessor = fruit.getValue(); const auto& fruitValue = fruitAccessor.value(); - suite.expectEqual(fruitValue, "fruit"); + expect(fruitValue) == "fruit"; auto branches = fruit.at("Elements").value(); - suite.expectEqual(std::ranges::size(branches), 1uz); + expect(std::ranges::size(branches)) == 1uz; { auto appleAccessor = branches[0].value().getValue(); const auto& appleValue = appleAccessor.value(); Logger::print("{}", appleValue); - suite.expectEqual(appleValue, "apple"); + expect(appleValue) == "apple"; } - }); + }; - suite.addTest("Operator==: Equal nodes", [&] { + suite.test("Operator==: Equal nodes") = [&] { auto node1 = StringMeshNodePtr::make("test"); auto node2 = node1; - suite.expectEqual(node1, node2); - }); + expect(node1) == node2; + }; - suite.addTest("Operator==: Unequal nodes", [&] { + suite.test("Operator==: Unequal nodes") = [&] { auto node1 = StringMeshNodePtr::make("test1"); auto node2 = StringMeshNodePtr::make("test2"); - suite.expect(node1 != node2); - }); + expect(node1) != node2; + }; - suite.addTest("Operator==: Assigned nodes", [&] { + suite.test("Operator==: Assigned nodes") = [&] { auto node1 = StringMeshNodePtr::make("test1"); auto node2 = StringMeshNodePtr::make("test2"); - suite.expect(node1 != node2); + expect(node1) != node2; node2 = node1; - suite.expectEqual(node1, node2); - }); + expect(node1) == node2; + }; - suite.addTest("Operator==: Node and null", [&] { + suite.test("Operator==: Node and null") = [&] { auto node1 = StringMeshNodePtr::make("test"); auto node2 = StringMeshNodePtr{}; - suite.expect(node1 != node2); - }); + expect(node1) != node2; + }; - suite.addTest("Detach child", [&] { + suite.test("Detach child") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); auto banana = StringMeshNodePtr::make("banana"); @@ -90,138 +90,138 @@ namespace CppUtils::UnitTest::Container::MeshNetwork { auto fruits = fruit.at("Elements").value(); - suite.expectEqual(std::ranges::size(fruits), 2uz); + expect(std::ranges::size(fruits)) == 2uz; { auto appleAccessor = fruits[0].value().getValue(); const auto& value = appleAccessor.value(); Logger::print("{}", value); - suite.expectEqual(value, "apple"); + expect(value) == "apple"; } { auto bananaAccessor = fruits[1].value().getValue(); const auto& value = bananaAccessor.value(); Logger::print("{}", value); - suite.expectEqual(value, "banana"); + expect(value) == "banana"; } } { fruit["Elements"] - apple; auto fruits = fruit.at("Elements").value(); - suite.expectEqual(std::ranges::size(fruits), 1uz); + expect(std::ranges::size(fruits)) == 1uz; { auto bananaAccessor = fruits[0].value().getValue(); const auto& bananaValue = bananaAccessor.value(); Logger::print("{}", bananaValue); - suite.expectEqual(bananaValue, "banana"); + expect(bananaValue) == "banana"; } } { fruit["Elements"] - banana; auto fruits = fruit.at("Elements").value_or({}); - suite.expectEqual(std::ranges::size(fruits), 0uz); + expect(std::ranges::size(fruits)) == 0uz; } - }); + }; - suite.addTest("Detach children", [&] { + suite.test("Detach children") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); auto banana = StringMeshNodePtr::make("banana"); fruit["Elements"] >> apple; fruit["Elements"] >> banana; - suite.expectEqual(std::ranges::size(fruit.at("Elements").value_or({})), 2uz); + expect(std::ranges::size(fruit.at("Elements").value_or({}))) == 2uz; fruit["Elements"].clear(); - suite.expectEqual(std::ranges::size(fruit.at("Elements").value_or({})), 0uz); - }); + expect(std::ranges::size(fruit.at("Elements").value_or({}))) == 0uz; + }; - suite.addTest("Detach child with multiple links", [&] { + suite.test("Detach child with multiple links") = [&] { auto elements = StringMeshNodePtr::makeRoot("elements"); auto orange = StringMeshNodePtr::make("orange"); - suite.expectEqual(orange.getDistanceFromRoot(), std::numeric_limits::max()); + expect(orange.getDistanceFromRoot()) == std::numeric_limits::max(); elements["Color"] >> orange; elements["Fruit"] >> orange; { - suite.expectEqual(elements.count("Color"), 1uz); - suite.expectEqual(elements.count("Fruit"), 1uz); - suite.expectEqual(orange.getDistanceFromRoot(), 1uz); + expect(elements.count("Color")) == 1uz; + expect(elements.count("Fruit")) == 1uz; + expect(orange.getDistanceFromRoot()) == 1uz; } elements["Color"] - orange; { - suite.expectEqual(elements.count("Color"), 0uz); - suite.expectEqual(elements.count("Fruit"), 1uz); - suite.expectEqual(std::ranges::size(elements.at("Fruit").value_or({})), 1uz); - suite.expectEqual(orange.getDistanceFromRoot(), 1uz); + expect(elements.count("Color")) == 0uz; + expect(elements.count("Fruit")) == 1uz; + expect(std::ranges::size(elements.at("Fruit").value_or({}))) == 1uz; + expect(orange.getDistanceFromRoot()) == 1uz; } elements["Fruit"] - orange; { - suite.expectEqual(elements.count("Color"), 0uz); - suite.expectEqual(elements.count("Fruit"), 0uz); - suite.expectEqual(orange.getDistanceFromRoot(), std::numeric_limits::max()); + expect(elements.count("Color")) == 0uz; + expect(elements.count("Fruit")) == 0uz; + expect(orange.getDistanceFromRoot()) == std::numeric_limits::max(); } - }); + }; - suite.addTest("Bidirectional link", [&] { + suite.test("Bidirectional link") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); fruit["Elements"] & apple["Categories"]; { auto elements = fruit.at("Elements").value(); - suite.expectEqual(std::ranges::size(elements), 1uz); + expect(std::ranges::size(elements)) == 1uz; { auto appleAccessor = elements[0].value().getValue(); const auto& value = appleAccessor.value(); Logger::print("fruit elements contains {}", value); - suite.expectEqual(value, "apple"); + expect(value) == "apple"; } } { auto categories = apple.at("Categories").value(); - suite.expectEqual(std::ranges::size(categories), 1uz); + expect(std::ranges::size(categories)) == 1uz; { auto fruitAccessor = categories[0].value().getValue(); const auto& value = fruitAccessor.value(); Logger::print("apple category is {}", value); - suite.expectEqual(value, "fruit"); + expect(value) == "fruit"; } } - }); + }; - suite.addTest("Bidirectional link (reverse order)", [&] { + suite.test("Bidirectional link (reverse order)") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); apple["Categories"] & fruit["Elements"]; { auto elements = fruit.at("Elements").value(); - suite.expectEqual(std::ranges::size(elements), 1uz); + expect(std::ranges::size(elements)) == 1uz; { auto appleAccessor = elements[0].value().getValue(); const auto& value = appleAccessor.value(); Logger::print("fruit elements contains {}", value); - suite.expectEqual(value, "apple"); + expect(value) == "apple"; } } { auto categories = apple.at("Categories").value(); - suite.expectEqual(std::ranges::size(categories), 1uz); + expect(std::ranges::size(categories)) == 1uz; { auto fruitAccessor = categories[0].value().getValue(); const auto& value = fruitAccessor.value(); Logger::print("apple category is {}", value); - suite.expectEqual(value, "fruit"); + expect(value) == "fruit"; } } - }); + }; - suite.addTest("Duplicate attachment protection (operator>>)", [&] { + suite.test("Duplicate attachment protection (operator>>)") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto child = StringMeshNodePtr::make("child"); @@ -229,10 +229,10 @@ namespace CppUtils::UnitTest::Container::MeshNetwork root["Branch"] >> child; const auto branch = root.at("Branch").value(); - suite.expectEqual(std::ranges::size(branch), 1uz); - }); + expect(std::ranges::size(branch)) == 1uz; + }; - suite.addTest("Duplicate attachment protection (operator&)", [&] { + suite.test("Duplicate attachment protection (operator&)") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); @@ -241,13 +241,13 @@ namespace CppUtils::UnitTest::Container::MeshNetwork apple["Categories"] & fruit["Elements"]; const auto elements = fruit.at("Elements").value(); - suite.expectEqual(std::ranges::size(elements), 1uz); + expect(std::ranges::size(elements)) == 1uz; const auto categories = apple.at("Categories").value(); - suite.expectEqual(std::ranges::size(categories), 1uz); - }); + expect(std::ranges::size(categories)) == 1uz; + }; - suite.addTest("Many", [&] { + suite.test("Many") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto banana = StringMeshNodePtr::make("banana"); auto orangeFruit = StringMeshNodePtr::make("orange"); @@ -270,15 +270,15 @@ namespace CppUtils::UnitTest::Container::MeshNetwork { auto fruits = fruit.at("Elements").value(); - suite.expectEqual(std::ranges::size(fruits), 3uz); + expect(std::ranges::size(fruits)) == 3uz; for (auto node : fruits) { - suite.expectEqual(std::ranges::size(node.at("Categories").value_or({})), 1uz); + expect(std::ranges::size(node.at("Categories").value_or({}))) == 1uz; auto categoryAccessor = node["Categories"][0].value().getValue(); const auto& categoryValue = categoryAccessor.value(); - suite.expectEqual(categoryValue, "fruit"); + expect(categoryValue) == "fruit"; - suite.expectEqual(std::ranges::size(node.at("Colors").value_or({})), 1uz); + expect(std::ranges::size(node.at("Colors").value_or({}))) == 1uz; auto colorAccessor = node["Colors"][0].value().getValue(); auto fruitAccessor = node.getValue(); const auto& colorValue = colorAccessor.value(); @@ -287,19 +287,19 @@ namespace CppUtils::UnitTest::Container::MeshNetwork Logger::print("{} is a {} {}", fruitValue, colorValue, categoryValue); } } - }); + }; - suite.addTest("Attach (operator>>)", [&] { + suite.test("Attach (operator>>)") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); fruit["Elements"] >> apple; const auto& elements = fruit.at("Elements").value_or({}); - suite.expectEqual(std::ranges::size(elements), 1uz); - suite.expectEqual(elements[0].value().getValue().value(), "apple"); - }); + expect(std::ranges::size(elements)) == 1uz; + expect(elements[0].value().getValue().value()) == "apple"; + }; - suite.addTest("Chaining (operator>>)", [&] { + suite.test("Chaining (operator>>)") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto child1 = StringMeshNodePtr::make("child1"); auto child2 = StringMeshNodePtr::make("child2"); @@ -307,95 +307,95 @@ namespace CppUtils::UnitTest::Container::MeshNetwork root["Branch1"] >> child1["Branch2"] >> child2; const auto& branch1Children = root.at("Branch1").value_or({}); - suite.expectEqual(std::ranges::size(branch1Children), 1uz); - suite.expectEqual(branch1Children[0].value().getValue().value(), "child1"); + expect(std::ranges::size(branch1Children)) == 1uz; + expect(branch1Children[0].value().getValue().value()) == "child1"; const auto& branch2Children = branch1Children[0].value().at("Branch2").value_or({}); - suite.expectEqual(std::ranges::size(branch2Children), 1uz); - suite.expectEqual(branch2Children[0].value().getValue().value(), "child2"); - }); + expect(std::ranges::size(branch2Children)) == 1uz; + expect(branch2Children[0].value().getValue().value()) == "child2"; + }; - suite.addTest("Unlink (operator-)", [&] { + suite.test("Unlink (operator-)") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto child = StringMeshNodePtr::make("child"); root["Branch"] >> child; - suite.expectEqual(std::ranges::size(root.at("Branch").value_or({})), 1uz); + expect(std::ranges::size(root.at("Branch").value_or({}))) == 1uz; root["Branch"] - child; - suite.expectEqual(std::ranges::size(root.at("Branch").value_or({})), 0uz); - }); + expect(std::ranges::size(root.at("Branch").value_or({}))) == 0uz; + }; - suite.addTest("Bidirectional link (operator&)", [&] { + suite.test("Bidirectional link (operator&)") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); fruit["Elements"] & apple["Categories"]; const auto& elements = fruit.at("Elements").value_or({}); - suite.expectEqual(std::ranges::size(elements), 1uz); - suite.expectEqual(elements[0].value().getValue().value(), "apple"); + expect(std::ranges::size(elements)) == 1uz; + expect(elements[0].value().getValue().value()) == "apple"; const auto& categories = apple.at("Categories").value_or({}); - suite.expectEqual(std::ranges::size(categories), 1uz); - suite.expectEqual(categories[0].value().getValue().value(), "fruit"); - }); + expect(std::ranges::size(categories)) == 1uz; + expect(categories[0].value().getValue().value()) == "fruit"; + }; - suite.addTest("Bidirectional unlink (operator/)", [&] { + suite.test("Bidirectional unlink (operator/)") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); fruit["Elements"] & apple["Categories"]; - suite.expectEqual(std::ranges::size(fruit.at("Elements").value_or({})), 1uz); - suite.expectEqual(std::ranges::size(apple.at("Categories").value_or({})), 1uz); + expect(std::ranges::size(fruit.at("Elements").value_or({}))) == 1uz; + expect(std::ranges::size(apple.at("Categories").value_or({}))) == 1uz; fruit["Elements"] / apple["Categories"]; - suite.expectEqual(std::ranges::size(fruit.at("Elements").value_or({})), 0uz); - suite.expectEqual(std::ranges::size(apple.at("Categories").value_or({})), 0uz); - }); + expect(std::ranges::size(fruit.at("Elements").value_or({}))) == 0uz; + expect(std::ranges::size(apple.at("Categories").value_or({}))) == 0uz; + }; - suite.addTest("Get value", [&] { + suite.test("Get value") = [&] { auto node = StringMeshNodePtr::make("apple"); auto accessor = node.getValue(); - suite.expectEqual(accessor.value(), "apple"); - }); + expect(accessor.value()) == "apple"; + }; - suite.addTest("Set value", [&] { + suite.test("Set value") = [&] { auto node = StringMeshNodePtr::make("initial"); node.setValue("updated"); auto accessor = node.getValue(); - suite.expectEqual(accessor.value(), "updated"); - }); + expect(accessor.value()) == "updated"; + }; - suite.addTest("Contains", [&] { + suite.test("Contains") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); fruit["Elements"] >> apple; - suite.expect(fruit.contains("Elements")); + expect(fruit.contains("Elements")); fruit["Elements"] - apple; - suite.expect(not fruit.contains("Elements")); - }); + expect(not fruit.contains("Elements")); + }; - suite.addTest("Count", [&] { + suite.test("Count") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto child = StringMeshNodePtr::make("child"); - suite.expectEqual(root.count("Branch"), 0uz); + expect(root.count("Branch")) == 0uz; root["Branch"] >> child; - suite.expectEqual(root.count("Branch"), 1uz); + expect(root.count("Branch")) == 1uz; root["Branch"] - child; - suite.expectEqual(root.count("Branch"), 0uz); - }); + expect(root.count("Branch")) == 0uz; + }; - suite.addTest("Branch size", [&] { + suite.test("Branch size") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto child1 = StringMeshNodePtr::make("child1"); auto child2 = StringMeshNodePtr::make("child2"); root["Branch"] >> child1; root["Branch"] >> child2; - suite.expectEqual(std::ranges::size(root.at("Branch").value_or({})), 2uz); - }); + expect(std::ranges::size(root.at("Branch").value_or({}))) == 2uz; + }; - suite.addTest("Branch stays live after construction", [&] { + suite.test("Branch stays live after construction") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); { auto apple = StringMeshNodePtr::make("apple"); @@ -404,60 +404,60 @@ namespace CppUtils::UnitTest::Container::MeshNetwork } { const auto branch = fruit["Elements"]; - suite.expectEqual(std::ranges::size(branch), 1uz); - suite.expectEqual(branch.front().value().getValue().value(), "apple"); + expect(std::ranges::size(branch)) == 1uz; + expect(branch.front().value().getValue().value()) == "apple"; } - }); + }; - suite.addTest("Clear branch", [&] { + suite.test("Clear branch") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto child = StringMeshNodePtr::make("child"); root["Branch"] >> child; - suite.expectEqual(std::ranges::size(root.at("Branch").value_or({})), 1uz); + expect(std::ranges::size(root.at("Branch").value_or({}))) == 1uz; root["Branch"].clear(); - suite.expect(not root.contains("Branch")); - }); + expect(not root.contains("Branch")); + }; - suite.addTest("Get node", [&] { + suite.test("Get node") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); fruit["Elements"] >> apple; auto node = fruit["Elements"][0]; - suite.expect(node.has_value()); - suite.expectEqual(node.value().getValue().value(), "apple"); - }); + expect(node.has_value()); + expect(node.value().getValue().value()) == "apple"; + }; - suite.addTest("Get node: Newly created branch", [&] { + suite.test("Get node: Newly created branch") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto invalidKeyExpected = root["Branch"][0]; - suite.expect(not invalidKeyExpected); - suite.expectEqual(invalidKeyExpected.error(), "Branch index out of bounds"sv); - }); + expect(not invalidKeyExpected); + expect(invalidKeyExpected.error()) == "Branch index out of bounds"; + }; - suite.addTest("Get node: Invalid index", [&] { + suite.test("Get node: Invalid index") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); fruit["Elements"] >> apple; auto invalidIndexExpected = fruit["Elements"][1]; - suite.expect(not invalidIndexExpected); - suite.expectEqual(invalidIndexExpected.error(), "Branch index out of bounds"sv); - }); + expect(not invalidIndexExpected); + expect(invalidIndexExpected.error()) == "Branch index out of bounds"; + }; - suite.addTest("Get node: Invalid node", [&] { + suite.test("Get node: Invalid node") = [&] { auto emptyNode = StringMeshNodePtr{}; - suite.expectThrow([&] { + expect([&] { auto _ = emptyNode["Branch"]; - }); - }); + }) == throws; + }; - suite.addTest("Get nodes", [&] { + suite.test("Get nodes") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); auto banana = StringMeshNodePtr::make("banana"); @@ -468,56 +468,56 @@ namespace CppUtils::UnitTest::Container::MeshNetwork for (const auto& node : fruit["Elements"]) children.push_back(node); - suite.expectEqual(std::ranges::size(children), 2uz); - suite.expectEqual(children[0].getValue().value(), "apple"); - suite.expectEqual(children[1].getValue().value(), "banana"); - }); + expect(std::ranges::size(children)) == 2uz; + expect(children[0].getValue().value()) == "apple"; + expect(children[1].getValue().value()) == "banana"; + }; - suite.addTest("Get nodes: Non-existent branch", [&] { + suite.test("Get nodes: Non-existent branch") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto children = std::vector{}; for (const auto& child : root["NonExistentBranch"]) children.push_back(child); - suite.expect(std::ranges::empty(children)); - }); + expect(std::ranges::empty(children)); + }; - suite.addTest("Get nodes: Invalid node", [&] { + suite.test("Get nodes: Invalid node") = [&] { auto emptyNode = StringMeshNodePtr{}; - suite.expectThrow([&] { + expect([&] { auto _ = emptyNode["Branch"]; - }); - }); + }) == throws; + }; - suite.addTest("at()", [&] { + suite.test("at()") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); fruit["Elements"] >> apple; const auto branch = fruit.at("Elements"); - suite.expect(branch.has_value()); - suite.expectEqual(std::ranges::size(branch.value()), 1uz); - suite.expectEqual(branch.value()[0].value().getValue().value(), "apple"); - }); + expect(branch.has_value()); + expect(std::ranges::size(branch.value())) == 1uz; + expect(branch.value()[0].value().getValue().value()) == "apple"; + }; - suite.addTest("at(): Non-existent branch", [&] { + suite.test("at(): Non-existent branch") = [&] { const auto root = StringMeshNodePtr::makeRoot("root"); auto branch = root.at("NonExistentBranch"); - suite.expect(not branch.has_value()); - suite.expectEqual(branch.error(), "MeshNodePtr::at(): Key not found"sv); - }); + expect(not branch.has_value()); + expect(branch.error()) == "MeshNodePtr::at(): Key not found"; + }; - suite.addTest("at(): Invalid node", [&] { + suite.test("at(): Invalid node") = [&] { auto node = StringMeshNodePtr{}; auto branch = node.at("Branch"); - suite.expect(not branch.has_value()); - suite.expectEqual(branch.error(), "MeshNodePtr::at(): Invalid MeshNodePtr"sv); - }); + expect(not branch.has_value()); + expect(branch.error()) == "MeshNodePtr::at(): Invalid MeshNodePtr"; + }; - suite.addTest("Get first node", [&] { + suite.test("Get first node") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); auto banana = StringMeshNodePtr::make("banana"); @@ -525,11 +525,11 @@ namespace CppUtils::UnitTest::Container::MeshNetwork fruit["Elements"] >> banana; auto node = fruit["Elements"].front(); - suite.expect(node.has_value()); - suite.expectEqual(node.value().getValue().value(), "apple"); - }); + expect(node.has_value()); + expect(node.value().getValue().value()) == "apple"; + }; - suite.addTest("Get last node", [&] { + suite.test("Get last node") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); auto apple = StringMeshNodePtr::make("apple"); auto banana = StringMeshNodePtr::make("banana"); @@ -537,11 +537,11 @@ namespace CppUtils::UnitTest::Container::MeshNetwork fruit["Elements"] >> banana; auto node = fruit["Elements"].back(); - suite.expect(node.has_value()); - suite.expectEqual(node.value().getValue().value(), "banana"); - }); + expect(node.has_value()); + expect(node.value().getValue().value()) == "banana"; + }; - suite.addTest("Random access iterator", [&] { + suite.test("Random access iterator") = [&] { auto fruit = StringMeshNodePtr::makeRoot("fruit"); { auto apple = StringMeshNodePtr::make("apple"); @@ -555,20 +555,20 @@ namespace CppUtils::UnitTest::Container::MeshNetwork auto branch = fruit["Elements"]; auto it = std::ranges::begin(branch); - suite.expectEqual((*it).getValue().value(), "apple"); - suite.expectEqual((*(it + 2)).getValue().value(), "orange"); - suite.expectEqual((it[1]).getValue().value(), "banana"); + expect((*it).getValue().value()) == "apple"; + expect((*(it + 2)).getValue().value()) == "orange"; + expect((it[1]).getValue().value()) == "banana"; it += 2; - suite.expectEqual((*it).getValue().value(), "orange"); + expect((*it).getValue().value()) == "orange"; it -= 1; - suite.expectEqual((*it).getValue().value(), "banana"); + expect((*it).getValue().value()) == "banana"; - suite.expect(it < std::ranges::cend(branch)); - suite.expectEqual(std::ranges::cend(branch) - std::ranges::cbegin(branch), 3); - }); + expect(it) < std::ranges::cend(branch); + expect(std::ranges::cend(branch) - std::ranges::cbegin(branch)) == 3; + }; - suite.addTest("Get branches", [&] { + suite.test("Get branches") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto child1 = StringMeshNodePtr::make("child1"); auto child2 = StringMeshNodePtr::make("child2"); @@ -581,18 +581,18 @@ namespace CppUtils::UnitTest::Container::MeshNetwork auto branchesAccessor = root.getBranches(); const auto& branches = branchesAccessor.value(); - suite.expectEqual(std::ranges::size(branches), 2uz); - suite.expect(branches.contains("A")); - suite.expect(branches.contains("B")); + expect(std::ranges::size(branches)) == 2uz; + expect(branches.contains("A")); + expect(branches.contains("B")); auto aBranches = branches.at("A")->sharedAccess(); - suite.expectEqual(std::ranges::size(aBranches.value()), 1uz); + expect(std::ranges::size(aBranches.value())) == 1uz; auto bBranches = branches.at("B")->sharedAccess(); - suite.expectEqual(std::ranges::size(bBranches.value()), 2uz); - }); + expect(std::ranges::size(bBranches.value())) == 2uz; + }; - suite.addTest("Clear all branches", [&] { + suite.test("Clear all branches") = [&] { auto root = StringMeshNodePtr::makeRoot("root"); auto childA = StringMeshNodePtr::make("childA"); auto childB = StringMeshNodePtr::make("childB"); @@ -600,11 +600,11 @@ namespace CppUtils::UnitTest::Container::MeshNetwork root["BranchA"] >> childA; root["BranchB"] >> childB; - suite.expectEqual(std::ranges::size(root.getBranches().value()), 2uz); + expect(std::ranges::size(root.getBranches().value())) == 2uz; root.clearBranches(); - suite.expect(std::ranges::empty(root.getBranches().value())); - }); - }}; + expect(std::ranges::empty(root.getBranches().value())); + }; + }; } diff --git a/tests/Container/MultiKeyMap.mpp b/tests/Container/MultiKeyMap.mpp index 64b8c137..7313e72b 100644 --- a/tests/Container/MultiKeyMap.mpp +++ b/tests/Container/MultiKeyMap.mpp @@ -13,7 +13,7 @@ namespace CppUtils::UnitTest::Container::MultiKeyMap [[nodiscard]] inline constexpr auto operator==(const UserData& other) const noexcept -> bool = default; }; - auto _ = TestSuite{"Container/MultiKeyMap", {"UnitTest"}, [](TestSuite& suite) { + auto _ = testSuite("Container/MultiKeyMap").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace std::literals; using UserMap = CppUtils::Container::MultiKeyMap< @@ -28,75 +28,75 @@ namespace CppUtils::UnitTest::Container::MultiKeyMap static_assert(std::ranges::bidirectional_range); static_assert(std::ranges::random_access_range); - suite.addTest("Insertion and emplace", [&] { + suite.test("Insertion and emplace") = [&] { auto users = UserMap{}; - suite.expect(std::ranges::empty(users)); - suite.expectEqual(std::ranges::size(users), 0uz); + expect(std::ranges::empty(users)); + expect(std::ranges::size(users)) == 0uz; const auto [aliceIterator, insertedAlice] = users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); - suite.expect(insertedAlice); - suite.expect(aliceIterator != std::ranges::end(users)); - suite.expectEqual(aliceIterator->value.age, 30); + expect(insertedAlice); + expect(aliceIterator) != std::ranges::end(users); + expect(aliceIterator->value.age) == 30; const auto [bobIterator, insertedBob] = users.emplace(2, "Bob"s, "Design"sv, UserData{.age = 28, .role = "UI"}); - suite.expect(insertedBob); - suite.expect(bobIterator != std::ranges::end(users)); + expect(insertedBob); + expect(bobIterator) != std::ranges::end(users); - suite.expectEqual(std::ranges::size(users), 2uz); - suite.expect(not std::ranges::empty(users)); - }); + expect(std::ranges::size(users)) == 2uz; + expect(not std::ranges::empty(users)); + }; - suite.addTest("Direct construction with makeEntry", [&] { + suite.test("Direct construction with makeEntry") = [&] { const auto users = UserMap{ UserMap::makeEntry(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}), UserMap::makeEntry(2, "Bob"s, "Design"sv, UserData{.age = 28, .role = "UI"})}; - suite.expectEqual(std::ranges::size(users), 2uz); - suite.expect(users.contains(1)); - suite.expect(users.contains("Alice"s)); - suite.expect(users.contains(2)); - suite.expect(users.contains("Bob"s)); - suite.expectEqual(users.at(1), UserData{.age = 30, .role = "Dev"}); - suite.expectEqual(users.at("Bob"s), UserData{.age = 28, .role = "UI"}); - suite.expectEqual(users.at(1).age, 30); - suite.expectEqual(users.at("Bob"s).role, "UI"s); - }); - - suite.addTest("Variadic construction with move-only values", [&] { + expect(std::ranges::size(users)) == 2uz; + expect(users.contains(1)); + expect(users.contains("Alice"s)); + expect(users.contains(2)); + expect(users.contains("Bob"s)); + expect(users.at(1)) == UserData{.age = 30, .role = "Dev"}; + expect(users.at("Bob"s)) == UserData{.age = 28, .role = "UI"}; + expect(users.at(1).age) == 30; + expect(users.at("Bob"s).role) == "UI"; + }; + + suite.test("Variadic construction with move-only values") = [&] { using MoveOnlyMap = CppUtils::Container::MultiKeyMap>; auto map = MoveOnlyMap{ MoveOnlyMap::makeEntry(1, "one"s, std::make_unique(100)), MoveOnlyMap::makeEntry(2, "two"s, std::make_unique(200))}; - suite.expectEqual(std::ranges::size(map), 2uz); - suite.expect(map.contains(1)); - suite.expect(map.contains("two"s)); - suite.expectEqual(*map.at(1), 100); - suite.expectEqual(*map.at("two"s), 200); - }); + expect(std::ranges::size(map)) == 2uz; + expect(map.contains(1)); + expect(map.contains("two"s)); + expect(*map.at(1)) == 100; + expect(*map.at("two"s)) == 200; + }; - suite.addTest("Type-deduced lookup by key type", [&] { + suite.test("Type-deduced lookup by key type") = [&] { auto users = UserMap{}; users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); users.insert(2, "Bob"s, "Design"sv, UserData{.age = 28, .role = "UI"}); // Automatic type-deduced find (int uniquely resolves to Key 0) const auto foundById = users.find(1); - suite.expect(foundById != std::ranges::end(users)); - suite.expectEqual(foundById->value, UserData{.age = 30, .role = "Dev"}); + expect(foundById) != std::ranges::end(users); + expect(foundById->value) == UserData{.age = 30, .role = "Dev"}; // Automatic type-deduced find (std::string uniquely resolves to Key 1) const auto foundByName = users.find("Bob"s); - suite.expect(foundByName != std::ranges::end(users)); - suite.expectEqual(foundByName->value, UserData{.age = 28, .role = "UI"}); + expect(foundByName) != std::ranges::end(users); + expect(foundByName->value) == UserData{.age = 28, .role = "UI"}; // Missing key const auto notFound = users.find(999); - suite.expect(notFound == std::ranges::end(users)); - }); + expect(notFound) == std::ranges::end(users); + }; - suite.addTest("Tagged key lookup and containment", [&] { + suite.test("Tagged key lookup and containment") = [&] { using TaggedMap = CppUtils::Container::MultiKeyMap< CppUtils::Container::TaggedKey<"Sender", int>, CppUtils::Container::TaggedKey<"Receiver", int>, @@ -106,21 +106,21 @@ namespace CppUtils::UnitTest::Container::MultiKeyMap messages.insert(100, 200, "Hello"s); messages.insert(300, 400, "World"s); - suite.expect(messages.contains<"Sender">(100)); - suite.expect(messages.contains<"Receiver">(200)); - suite.expect(not messages.contains<"Sender">(200)); - suite.expect(not messages.contains<"Receiver">(100)); + expect(messages.contains<"Sender">(100)); + expect(messages.contains<"Receiver">(200)); + expect(not messages.contains<"Sender">(200)); + expect(not messages.contains<"Receiver">(100)); const auto senderIterator = messages.find<"Sender">(100); - suite.expect(senderIterator != std::ranges::end(messages)); - suite.expectEqual(senderIterator->value, "Hello"s); + expect(senderIterator) != std::ranges::end(messages); + expect(senderIterator->value) == "Hello"; const auto receiverIterator = messages.find<"Receiver">(400); - suite.expect(receiverIterator != std::ranges::end(messages)); - suite.expectEqual(receiverIterator->value, "World"s); - }); + expect(receiverIterator) != std::ranges::end(messages); + expect(receiverIterator->value) == "World"; + }; - suite.addTest("Positional index lookup and containment", [&] { + suite.test("Positional index lookup and containment") = [&] { using TaggedMap = CppUtils::Container::MultiKeyMap< CppUtils::Container::TaggedKey<"Sender", int>, CppUtils::Container::TaggedKey<"Receiver", int>, @@ -130,95 +130,95 @@ namespace CppUtils::UnitTest::Container::MultiKeyMap messages.insert(100, 200, "Hello"s); messages.insert(300, 400, "World"s); - suite.expect(messages.containsAt<0uz>(100)); - suite.expect(messages.containsAt<1uz>(200)); - suite.expect(not messages.containsAt<0uz>(200)); - suite.expect(not messages.containsAt<1uz>(100)); + expect(messages.containsAt<0uz>(100)); + expect(messages.containsAt<1uz>(200)); + expect(not messages.containsAt<0uz>(200)); + expect(not messages.containsAt<1uz>(100)); const auto senderIterator = messages.findAt<0uz>(100); - suite.expect(senderIterator != std::ranges::end(messages)); - suite.expectEqual(senderIterator->value, "Hello"s); + expect(senderIterator) != std::ranges::end(messages); + expect(senderIterator->value) == "Hello"; const auto receiverIterator = messages.findAt<1uz>(400); - suite.expect(receiverIterator != std::ranges::end(messages)); - suite.expectEqual(receiverIterator->value, "World"s); - }); + expect(receiverIterator) != std::ranges::end(messages); + expect(receiverIterator->value) == "World"; + }; - suite.addTest("Key containment and count queries", [&] { + suite.test("Key containment and count queries") = [&] { auto users = UserMap{}; users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); - suite.expect(users.contains(1)); - suite.expect(users.contains("Alice"s)); - suite.expect(not users.contains(999)); - suite.expect(not users.contains("Unknown"s)); + expect(users.contains(1)); + expect(users.contains("Alice"s)); + expect(not users.contains(999)); + expect(not users.contains("Unknown"s)); - suite.expectEqual(users.count(1), 1uz); - suite.expectEqual(users.count("Alice"s), 1uz); - suite.expectEqual(users.count(999), 0uz); - suite.expectEqual(users.count("Unknown"s), 0uz); - }); + expect(users.count(1)) == 1uz; + expect(users.count("Alice"s)) == 1uz; + expect(users.count(999)) == 0uz; + expect(users.count("Unknown"s)) == 0uz; + }; - suite.addTest("Element access with at() and operator[]", [&] { + suite.test("Element access with at() and operator[]") = [&] { auto users = UserMap{}; users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); users.insert(2, "Bob"s, "Design"sv, UserData{.age = 28, .role = "UI"}); // at() reading - suite.expectEqual(users.at(1), UserData{.age = 30, .role = "Dev"}); - suite.expectEqual(users.at("Alice"s), UserData{.age = 30, .role = "Dev"}); - suite.expectThrow([&] { auto _ = users.at(999); }); + expect(users.at(1)) == UserData{.age = 30, .role = "Dev"}; + expect(users.at("Alice"s)) == UserData{.age = 30, .role = "Dev"}; + expect([&] { auto _ = users.at(999); }) == throws; // operator[] reading and mutation users[1].age = 31; - suite.expectEqual(users.at(1), UserData{.age = 31, .role = "Dev"}); + expect(users.at(1)) == UserData{.age = 31, .role = "Dev"}; users["Bob"s].role = "Senior UI"; - suite.expectEqual(users.at(2), UserData{.age = 28, .role = "Senior UI"}); - }); + expect(users.at(2)) == UserData{.age = 28, .role = "Senior UI"}; + }; - suite.addTest("Unique constraints across multiple keys", [&] { + suite.test("Unique constraints across multiple keys") = [&] { auto users = UserMap{}; users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); // Duplicate identifier (Key 0) const auto [duplicateIdentifierIterator, insertedDuplicateId] = users.insert(1, "Charlie"s, "Engineering"sv, UserData{.age = 22, .role = "Junior"}); - suite.expect(not insertedDuplicateId); - suite.expect(duplicateIdentifierIterator == std::ranges::end(users)); + expect(not insertedDuplicateId); + expect(duplicateIdentifierIterator) == std::ranges::end(users); // Duplicate name (Key 1) const auto [duplicateNameIterator, insertedDuplicateName] = users.insert(3, "Alice"s, "Marketing"sv, UserData{.age = 35, .role = "Lead"}); - suite.expect(not insertedDuplicateName); - suite.expect(duplicateNameIterator == std::ranges::end(users)); + expect(not insertedDuplicateName); + expect(duplicateNameIterator) == std::ranges::end(users); - suite.expectEqual(std::ranges::size(users), 1uz); - }); + expect(std::ranges::size(users)) == 1uz; + }; - suite.addTest("Non-unique key range lookup", [&] { + suite.test("Non-unique key range lookup") = [&] { auto users = UserMap{}; users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); users.insert(2, "Bob"s, "Engineering"sv, UserData{.age = 28, .role = "Dev"}); users.insert(3, "Charlie"s, "Design"sv, UserData{.age = 25, .role = "UI"}); - suite.expectEqual(users.count("Engineering"sv), 2uz); - suite.expectEqual(users.count("Design"sv), 1uz); - suite.expectEqual(users.count("HR"sv), 0uz); + expect(users.count("Engineering"sv)) == 2uz; + expect(users.count("Design"sv)) == 1uz; + expect(users.count("HR"sv)) == 0uz; const auto engineers = users.find("Engineering"sv); static_assert(std::ranges::random_access_range); - suite.expectEqual(std::ranges::size(engineers), 2uz); - suite.expect(not std::ranges::empty(engineers)); + expect(std::ranges::size(engineers)) == 2uz; + expect(not std::ranges::empty(engineers)); const auto engineeringAges = engineers | std::views::transform([](const auto& engineer) { return engineer.value.age; }) | std::ranges::to(); - suite.expectEqual(std::ranges::size(engineeringAges), 2uz); - suite.expect(std::ranges::contains(engineeringAges, 30)); - suite.expect(std::ranges::contains(engineeringAges, 28)); - }); + expect(std::ranges::size(engineeringAges)) == 2uz; + expect(std::ranges::contains(engineeringAges, 30)); + expect(std::ranges::contains(engineeringAges, 28)); + }; - suite.addTest("Erase by key and swap-and-pop integrity", [&] { + suite.test("Erase by key and swap-and-pop integrity") = [&] { auto users = UserMap{}; users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); users.insert(2, "Bob"s, "Engineering"sv, UserData{.age = 28, .role = "Dev"}); @@ -226,41 +226,41 @@ namespace CppUtils::UnitTest::Container::MultiKeyMap // Erase by unique int key const auto erasedAlice = users.erase(1); - suite.expect(erasedAlice); - suite.expectEqual(std::ranges::size(users), 2uz); - suite.expect(not users.contains(1)); - suite.expect(not users.contains("Alice"s)); + expect(erasedAlice); + expect(std::ranges::size(users)) == 2uz; + expect(not users.contains(1)); + expect(not users.contains("Alice"s)); // Bob and Charlie must still be accessible via all keys after swap-and-pop - suite.expect(users.contains(2)); - suite.expect(users.contains("Bob"s)); - suite.expect(users.contains(3)); - suite.expect(users.contains("Charlie"s)); - suite.expectEqual(users.count("Engineering"sv), 1uz); + expect(users.contains(2)); + expect(users.contains("Bob"s)); + expect(users.contains(3)); + expect(users.contains("Charlie"s)); + expect(users.count("Engineering"sv)) == 1uz; // Erase by unique string key const auto erasedBob = users.erase("Bob"s); - suite.expect(erasedBob); - suite.expectEqual(std::ranges::size(users), 1uz); - suite.expect(users.contains(3)); - }); + expect(erasedBob); + expect(std::ranges::size(users)) == 1uz; + expect(users.contains(3)); + }; - suite.addTest("Erase by iterator", [&] { + suite.test("Erase by iterator") = [&] { auto users = UserMap{}; users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); users.insert(2, "Bob"s, "Engineering"sv, UserData{.age = 28, .role = "Dev"}); const auto iterator = std::ranges::begin(users); const auto nextIterator = users.erase(iterator); - suite.expectEqual(std::ranges::size(users), 1uz); - suite.expect(nextIterator == std::ranges::begin(users)); + expect(std::ranges::size(users)) == 1uz; + expect(nextIterator) == std::ranges::begin(users); users.erase(nextIterator); - suite.expect(std::ranges::empty(users)); - suite.expectEqual(std::ranges::size(users), 0uz); - }); + expect(std::ranges::empty(users)); + expect(std::ranges::size(users)) == 0uz; + }; - suite.addTest("Standard range algorithms and views pipeline", [&] { + suite.test("Standard range algorithms and views pipeline") = [&] { auto users = UserMap{}; users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); users.insert(2, "Bob"s, "Engineering"sv, UserData{.age = 28, .role = "Dev"}); @@ -271,14 +271,14 @@ namespace CppUtils::UnitTest::Container::MultiKeyMap std::ranges::for_each(users, [&](const auto& entry) { totalAge += entry.value.age; }); - suite.expectEqual(totalAge, 83); + expect(totalAge) == 83; // std::ranges::max_element const auto oldest = std::ranges::max_element(users, {}, [](const auto& entry) { return entry.value.age; }); - suite.expect(oldest != std::ranges::end(users)); - suite.expectEqual(oldest->value.age, 30); + expect(oldest) != std::ranges::end(users); + expect(oldest->value.age) == 30; // Views pipeline const auto collectedRoles = users @@ -286,10 +286,10 @@ namespace CppUtils::UnitTest::Container::MultiKeyMap | std::views::transform([](const auto& entry) { return entry.value.role; }) | std::ranges::to(); - suite.expectEqual(std::ranges::size(collectedRoles), 2uz); - }); + expect(std::ranges::size(collectedRoles)) == 2uz; + }; - suite.addTest("Structured binding on entries", [&] { + suite.test("Structured binding on entries") = [&] { auto users = UserMap{}; users.insert(1, "Alice"s, "Engineering"sv, UserData{.age = 30, .role = "Dev"}); users.insert(2, "Bob"s, "Engineering"sv, UserData{.age = 28, .role = "Dev"}); @@ -298,76 +298,76 @@ namespace CppUtils::UnitTest::Container::MultiKeyMap auto boundNames = std::vector{}; for (const auto& [id, name, department, data] : users) { - suite.expect(id > 0); - suite.expect(not std::ranges::empty(name)); + expect(id) > 0; + expect(not std::ranges::empty(name)); boundNames.push_back(name); } - suite.expectEqual(std::ranges::size(boundNames), 3uz); - }); + expect(std::ranges::size(boundNames)) == 3uz; + }; - suite.addTest("Single key map with value", [&] { + suite.test("Single key map with value") = [&] { using SingleKeyMap = CppUtils::Container::MultiKeyMap; auto map = SingleKeyMap{}; const auto [firstIterator, insertedFirst] = map.insert(1, "one"s); - suite.expect(insertedFirst); - suite.expect(firstIterator != std::ranges::end(map)); - suite.expectEqual(firstIterator->value, "one"s); + expect(insertedFirst); + expect(firstIterator) != std::ranges::end(map); + expect(firstIterator->value) == "one"; map.emplace(2, "two"s); - suite.expectEqual(std::ranges::size(map), 2uz); + expect(std::ranges::size(map)) == 2uz; // Duplicate key rejection const auto [duplicateIterator, insertedDuplicate] = map.insert(1, "duplicate"s); - suite.expect(not insertedDuplicate); - suite.expect(duplicateIterator == std::ranges::end(map)); + expect(not insertedDuplicate); + expect(duplicateIterator) == std::ranges::end(map); // Lookup & access - suite.expect(map.contains(1)); - suite.expect(not map.contains(99)); - suite.expectEqual(map.count(1), 1uz); - suite.expectEqual(map.count(99), 0uz); + expect(map.contains(1)); + expect(not map.contains(99)); + expect(map.count(1)) == 1uz; + expect(map.count(99)) == 0uz; const auto foundIterator = map.find(2); - suite.expect(foundIterator != std::ranges::end(map)); - suite.expectEqual(foundIterator->value, "two"s); + expect(foundIterator) != std::ranges::end(map); + expect(foundIterator->value) == "two"; - suite.expectEqual(map.at(1), "one"s); + expect(map.at(1)) == "one"; map[1] = "uno"s; - suite.expectEqual(map.at(1), "uno"s); + expect(map.at(1)) == "uno"; map[3] = "three"s; - suite.expectEqual(std::ranges::size(map), 3uz); - suite.expectEqual(map.at(3), "three"s); + expect(std::ranges::size(map)) == 3uz; + expect(map.at(3)) == "three"; // Erase const auto erased = map.erase(1); - suite.expect(erased); - suite.expectEqual(std::ranges::size(map), 2uz); - suite.expect(not map.contains(1)); - }); + expect(erased); + expect(std::ranges::size(map)) == 2uz; + expect(not map.contains(1)); + }; - suite.addTest("Value=void bidirectional key container", [&] { + suite.test("Value=void bidirectional key container") = [&] { using BiKeyMap = CppUtils::Container::MultiKeyMap; auto pairs = BiKeyMap{}; pairs.insert(1, "one"s); pairs.insert(2, "two"s); - suite.expectEqual(std::ranges::size(pairs), 2uz); - suite.expect(pairs.contains(1)); - suite.expect(pairs.contains("one"s)); + expect(std::ranges::size(pairs)) == 2uz; + expect(pairs.contains(1)); + expect(pairs.contains("one"s)); // at() on void value returns the counterpart key - suite.expectEqual(pairs.at(1), "one"s); - suite.expectEqual(pairs.at("two"s), 2); + expect(pairs.at(1)) == "one"; + expect(pairs.at("two"s)) == 2; // Structured binding for (const auto& [number, text] : pairs) - suite.expect(number > 0 and not std::ranges::empty(text)); - }); + expect(number > 0 and not std::ranges::empty(text)); + }; - suite.addTest("Three keys and void value container", [&] { + suite.test("Three keys and void value container") = [&] { using TripleKeyMap = CppUtils::Container::MultiKeyMap< CppUtils::Container::TaggedKey<"Id", int>, CppUtils::Container::TaggedKey<"Name", std::string>, @@ -376,56 +376,56 @@ namespace CppUtils::UnitTest::Container::MultiKeyMap auto records = TripleKeyMap{}; const auto [aliceIterator, insertedAlice] = records.insert(1, "Alice"s, 95.5); - suite.expect(insertedAlice); - suite.expect(aliceIterator != std::ranges::end(records)); + expect(insertedAlice); + expect(aliceIterator) != std::ranges::end(records); records.insert(2, "Bob"s, 82.0); - suite.expectEqual(std::ranges::size(records), 2uz); + expect(std::ranges::size(records)) == 2uz; // Uniqueness collision on any of the three keys const auto [duplicateIdIterator, insertedDuplicateId] = records.insert(1, "Charlie"s, 70.0); - suite.expect(not insertedDuplicateId); - suite.expect(duplicateIdIterator == std::ranges::end(records)); + expect(not insertedDuplicateId); + expect(duplicateIdIterator) == std::ranges::end(records); const auto [duplicateNameIterator, insertedDuplicateName] = records.insert(3, "Alice"s, 60.0); - suite.expect(not insertedDuplicateName); - suite.expect(duplicateNameIterator == std::ranges::end(records)); + expect(not insertedDuplicateName); + expect(duplicateNameIterator) == std::ranges::end(records); const auto [duplicateScoreIterator, insertedDuplicateScore] = records.insert(4, "David"s, 95.5); - suite.expect(not insertedDuplicateScore); - suite.expect(duplicateScoreIterator == std::ranges::end(records)); + expect(not insertedDuplicateScore); + expect(duplicateScoreIterator) == std::ranges::end(records); // Lookups on each axis - suite.expect(records.contains(1)); - suite.expect(records.contains("Bob"s)); - suite.expect(records.contains(95.5)); - suite.expect(not records.contains(999)); + expect(records.contains(1)); + expect(records.contains("Bob"s)); + expect(records.contains(95.5)); + expect(not records.contains(999)); const auto foundById = records.find<"Id">(1); - suite.expect(foundById != std::ranges::end(records)); - suite.expectEqual(foundById->template key<1uz>(), "Alice"s); - suite.expectEqual(foundById->template key(), 95.5); + expect(foundById) != std::ranges::end(records); + expect(foundById->template key<1uz>()) == "Alice"; + expect(foundById->template key()) == 95.5; const auto foundByName = records.find<"Name">("Bob"s); - suite.expect(foundByName != std::ranges::end(records)); - suite.expectEqual(foundByName->template key<0uz>(), 2); - suite.expectEqual(foundByName->template key(), 82.0); + expect(foundByName) != std::ranges::end(records); + expect(foundByName->template key<0uz>()) == 2; + expect(foundByName->template key()) == 82.0; // Structured binding for (const auto& [id, name, score] : records) { - suite.expect(id > 0); - suite.expect(not std::ranges::empty(name)); - suite.expect(score > 0.0); + expect(id) > 0; + expect(not std::ranges::empty(name)); + expect(score) > 0.0; } // Erase on any axis const auto erasedBob = records.erase("Bob"s); - suite.expect(erasedBob); - suite.expectEqual(std::ranges::size(records), 1uz); - suite.expect(not records.contains(2)); - suite.expect(not records.contains("Bob"s)); - suite.expect(not records.contains(82.0)); - }); - }}; + expect(erasedBob); + expect(std::ranges::size(records)) == 1uz; + expect(not records.contains(2)); + expect(not records.contains("Bob"s)); + expect(not records.contains(82.0)); + }; + }; } diff --git a/tests/Container/NetworkPtr.mpp b/tests/Container/NetworkPtr.mpp index d3690a43..a8f48641 100644 --- a/tests/Container/NetworkPtr.mpp +++ b/tests/Container/NetworkPtr.mpp @@ -5,38 +5,38 @@ import CppUtils; namespace CppUtils::UnitTest::Container::NetworkPtr { - auto _ = TestSuite{"Container/NetworkPtr", {"Logger", "Thread/UniqueLocker", "Container/SafeShared"}, [](TestSuite& suite) { + auto _ = testSuite("Container/NetworkPtr").dependsOn("Logger", "Thread/UniqueLocker", "Container/SafeShared") = [](TestSuite& suite) { using Logger = TestSuite::Logger; using LifetimeChecker = CppUtils::UnitTest::LifetimeChecker; using NetworkPtr = CppUtils::Container::NetworkPtr; using StringNetworkPtr = CppUtils::Container::NetworkPtr; auto logger = [](std::string_view message) { - Logger::emit<"info">("{}", message); + Logger::print("{}", message); }; - suite.addTest("Create root", [&] { + suite.test("Create root") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); auto accessor = root->sharedAccess(); - suite.expectEqual(accessor->getDistanceFromRoot(), 0uz); - }); + expect(accessor->getDistanceFromRoot()) == 0uz; + }; - suite.addTest("Create node", [&] { + suite.test("Create node") = [&] { auto node = NetworkPtr::make(logger, "Node"); auto accessor = node->sharedAccess(); - suite.expectEqual(accessor->getDistanceFromRoot(), std::numeric_limits::max()); - }); + expect(accessor->getDistanceFromRoot()) == std::numeric_limits::max(); + }; - suite.addTest("Read value", [&] { + suite.test("Read value") = [&] { auto node = StringNetworkPtr::make("Node"); { auto accessor = node->sharedAccess(); - suite.expectEqual(accessor->getDistanceFromRoot(), std::numeric_limits::max()); - suite.expectEqual(accessor->value, "Node"); + expect(accessor->getDistanceFromRoot()) == std::numeric_limits::max(); + expect(accessor->value) == "Node"; } - }); + }; - suite.addTest("Write value", [&] { + suite.test("Write value") = [&] { auto node = StringNetworkPtr::make("Node"); { auto accessor = node->uniqueAccess(); @@ -44,25 +44,25 @@ namespace CppUtils::UnitTest::Container::NetworkPtr } { auto accessor = node->sharedAccess(); - suite.expectEqual(accessor->value, "Changed"); + expect(accessor->value) == "Changed"; } - }); + }; - suite.addTest("Create branch", [&] { + suite.test("Create branch") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); { auto branch = NetworkPtr::make(logger, "Branch", 1uz); auto accessor = CppUtils::Thread::MultipleAccessor{*root, *branch}; auto& rootRef = std::get<0>(accessor.values); auto& branchRef = std::get<1>(accessor.values); - suite.expectEqual(branchRef.getDistanceFromRoot(), std::numeric_limits::max()); + expect(branchRef.getDistanceFromRoot()) == std::numeric_limits::max(); rootRef.attachChild(branchRef); - suite.expectEqual(branchRef.getDistanceFromRoot(), 1uz); + expect(branchRef.getDistanceFromRoot()) == 1uz; } Logger::print("Persistance de Branch"); - }); + }; - suite.addTest("Create leaf", [&] { + suite.test("Create leaf") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); { auto branch = NetworkPtr::make(logger, "Branch", 1uz); @@ -71,8 +71,8 @@ namespace CppUtils::UnitTest::Container::NetworkPtr auto& rootRef = std::get<0>(accessor.values); auto& branchRef = std::get<1>(accessor.values); rootRef.attachChild(branchRef); - suite.expectEqual(rootRef.getDistanceFromRoot(), 0uz); - suite.expectEqual(branchRef.getDistanceFromRoot(), 1uz); + expect(rootRef.getDistanceFromRoot()) == 0uz; + expect(branchRef.getDistanceFromRoot()) == 1uz; } { auto leaf = NetworkPtr::make(logger, "Leaf", 2uz); @@ -80,15 +80,15 @@ namespace CppUtils::UnitTest::Container::NetworkPtr auto& branchRef = std::get<0>(accessor.values); auto& leafRef = std::get<1>(accessor.values); branchRef.attachChild(leafRef); - suite.expectEqual(branchRef.getDistanceFromRoot(), 1uz); - suite.expectEqual(leafRef.getDistanceFromRoot(), 2uz); + expect(branchRef.getDistanceFromRoot()) == 1uz; + expect(leafRef.getDistanceFromRoot()) == 2uz; } Logger::print("Persistance de Leaf"); } Logger::print("Persistance de Branch"); - }); + }; - suite.addTest("Cut leaf", [&] { + suite.test("Cut leaf") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); { auto leaf = NetworkPtr::make(logger, "Leaf", 1uz); @@ -96,22 +96,22 @@ namespace CppUtils::UnitTest::Container::NetworkPtr auto accessor = CppUtils::Thread::MultipleAccessor{*root, *leaf}; auto& rootRef = std::get<0>(accessor.values); auto& leafRef = std::get<1>(accessor.values); - suite.expectEqual(leafRef.getDistanceFromRoot(), std::numeric_limits::max()); + expect(leafRef.getDistanceFromRoot()) == std::numeric_limits::max(); rootRef.attachChild(leafRef); - suite.expectEqual(leafRef.getDistanceFromRoot(), 1uz); + expect(leafRef.getDistanceFromRoot()) == 1uz; } Logger::print("Persistance de Leaf"); auto accessor = CppUtils::Thread::MultipleAccessor{*root, *leaf}; auto& rootRef = std::get<0>(accessor.values); auto& leafRef = std::get<1>(accessor.values); - suite.expectEqual(leafRef.getDistanceFromRoot(), 1uz); + expect(leafRef.getDistanceFromRoot()) == 1uz; rootRef.detachChild(leafRef); - suite.expectEqual(leafRef.getDistanceFromRoot(), std::numeric_limits::max()); + expect(leafRef.getDistanceFromRoot()) == std::numeric_limits::max(); } Logger::print("Non-persistance de Leaf"); - }); + }; - suite.addTest("Cut branch with leaf", [&] { + suite.test("Cut branch with leaf") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); { auto branch = NetworkPtr::make(logger, "Branch", 1uz); @@ -132,19 +132,19 @@ namespace CppUtils::UnitTest::Container::NetworkPtr auto accessor = CppUtils::Thread::MultipleAccessor{*root, *branch}; auto& rootRef = std::get<0>(accessor.values); auto& branchRef = std::get<1>(accessor.values); - suite.expectEqual(branchRef.getDistanceFromRoot(), 1uz); + expect(branchRef.getDistanceFromRoot()) == 1uz; rootRef.detachChild(branchRef); - suite.expectEqual(branchRef.getDistanceFromRoot(), std::numeric_limits::max()); + expect(branchRef.getDistanceFromRoot()) == std::numeric_limits::max(); { auto leafAccessor = leaf->sharedAccess(); - suite.expectEqual(leafAccessor->getDistanceFromRoot(), std::numeric_limits::max()); + expect(leafAccessor->getDistanceFromRoot()) == std::numeric_limits::max(); } } Logger::print("Non-persistance de Branch"); - }); + }; - suite.addTest("Bidirectional link", [&] { + suite.test("Bidirectional link") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); auto branch = CppUtils::Container::SafeShared{}; { @@ -153,8 +153,8 @@ namespace CppUtils::UnitTest::Container::NetworkPtr auto& rootRef = std::get<0>(accessor.values); auto& branchRef = std::get<1>(accessor.values); rootRef.attachChild(branchRef); - suite.expectEqual(rootRef.getDistanceFromRoot(), 0uz); - suite.expectEqual(branchRef.getDistanceFromRoot(), 1uz); + expect(rootRef.getDistanceFromRoot()) == 0uz; + expect(branchRef.getDistanceFromRoot()) == 1uz; } Logger::print("Persistance de Branch"); { @@ -163,13 +163,13 @@ namespace CppUtils::UnitTest::Container::NetworkPtr auto& branchRef = std::get<1>(accessor.values); Logger::print("Création d'une boucle"); branchRef.attachChild(rootRef); - suite.expectEqual(rootRef.getDistanceFromRoot(), 0uz); - suite.expectEqual(branchRef.getDistanceFromRoot(), 1uz); + expect(rootRef.getDistanceFromRoot()) == 0uz; + expect(branchRef.getDistanceFromRoot()) == 1uz; } Logger::print("Libération de la boucle"); - }); + }; - suite.addTest("Detach child with multiple links", [&] { + suite.test("Detach child with multiple links") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); { auto leaf = NetworkPtr::make(logger, "Leaf", 1uz); @@ -177,35 +177,35 @@ namespace CppUtils::UnitTest::Container::NetworkPtr auto accessor = CppUtils::Thread::MultipleAccessor{*root, *leaf}; auto& rootRef = std::get<0>(accessor.values); auto& leafRef = std::get<1>(accessor.values); - suite.expectEqual(leafRef.getDistanceFromRoot(), std::numeric_limits::max()); + expect(leafRef.getDistanceFromRoot()) == std::numeric_limits::max(); rootRef.attachChild(leafRef); - suite.expectEqual(leafRef.getDistanceFromRoot(), 1uz); + expect(leafRef.getDistanceFromRoot()) == 1uz; rootRef.attachChild(leafRef); - suite.expectEqual(leafRef.getDistanceFromRoot(), 1uz); + expect(leafRef.getDistanceFromRoot()) == 1uz; rootRef.detachChild(leafRef); - suite.expectEqual(leafRef.getDistanceFromRoot(), 1uz); + expect(leafRef.getDistanceFromRoot()) == 1uz; rootRef.detachChild(leafRef); - suite.expectEqual(leafRef.getDistanceFromRoot(), std::numeric_limits::max()); + expect(leafRef.getDistanceFromRoot()) == std::numeric_limits::max(); } Logger::print("Non-persistance de Leaf"); } Logger::print("Persistance de Branch"); - }); + }; - suite.addTest("Attach and detach root on itself", [&] { + suite.test("Attach and detach root on itself") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); auto accessor = root->uniqueAccess(); accessor->attachChild(accessor.value()); - suite.expectEqual(accessor->getDistanceFromRoot(), 0uz); + expect(accessor->getDistanceFromRoot()) == 0uz; accessor->detachChild(accessor.value()); - suite.expectEqual(accessor->getDistanceFromRoot(), 0uz); - }); + expect(accessor->getDistanceFromRoot()) == 0uz; + }; - suite.addTest("Attach and detach node on itself", [&] { + suite.test("Attach and detach node on itself") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); auto node = NetworkPtr::make(logger, "Node", 1uz); { @@ -213,35 +213,35 @@ namespace CppUtils::UnitTest::Container::NetworkPtr auto& rootRef = std::get<0>(accessor.values); auto& nodeRef = std::get<1>(accessor.values); rootRef.attachChild(nodeRef); - suite.expectEqual(nodeRef.getDistanceFromRoot(), 1uz); + expect(nodeRef.getDistanceFromRoot()) == 1uz; } { auto accessor = node->uniqueAccess(); accessor->attachChild(accessor.value()); - suite.expectEqual(accessor->getDistanceFromRoot(), 1uz); + expect(accessor->getDistanceFromRoot()) == 1uz; accessor->detachChild(accessor.value()); - suite.expectEqual(accessor->getDistanceFromRoot(), 1uz); + expect(accessor->getDistanceFromRoot()) == 1uz; } { auto accessor = CppUtils::Thread::MultipleAccessor{*root, *node}; auto& rootRef = std::get<0>(accessor.values); auto& nodeRef = std::get<1>(accessor.values); rootRef.detachChild(nodeRef); - suite.expectEqual(nodeRef.getDistanceFromRoot(), std::numeric_limits::max()); + expect(nodeRef.getDistanceFromRoot()) == std::numeric_limits::max(); } - }); + }; - suite.addTest("Unidirectional link (operator>>)", [&] { + suite.test("Unidirectional link (operator>>)") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); auto child = NetworkPtr::make(logger, "Child", 1uz); root >> child; auto childAccessor = child->sharedAccess(); - suite.expectEqual(childAccessor->getDistanceFromRoot(), 1uz); - }); + expect(childAccessor->getDistanceFromRoot()) == 1uz; + }; - suite.addTest("Unidirectional link chaining (operator>>)", [&] { + suite.test("Unidirectional link chaining (operator>>)") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); auto branch = NetworkPtr::make(logger, "Branch", 1uz); auto leaf = NetworkPtr::make(logger, "Leaf", 2uz); @@ -250,30 +250,30 @@ namespace CppUtils::UnitTest::Container::NetworkPtr { auto branchAccessor = branch->sharedAccess(); - suite.expectEqual(branchAccessor->getDistanceFromRoot(), 1uz); + expect(branchAccessor->getDistanceFromRoot()) == 1uz; } { auto leafAccessor = leaf->sharedAccess(); - suite.expectEqual(leafAccessor->getDistanceFromRoot(), 2uz); + expect(leafAccessor->getDistanceFromRoot()) == 2uz; } - }); + }; - suite.addTest("Unlink (operator-)", [&] { + suite.test("Unlink (operator-)") = [&] { auto root = NetworkPtr::makeRoot(logger, "Root"); auto child = NetworkPtr::make(logger, "Child"); root >> child; { auto childAccessor = child->sharedAccess(); - suite.expectEqual(childAccessor->getDistanceFromRoot(), 1uz); + expect(childAccessor->getDistanceFromRoot()) == 1uz; } root - child; auto childAccessor = child->sharedAccess(); - suite.expectEqual(childAccessor->getDistanceFromRoot(), std::numeric_limits::max()); - }); + expect(childAccessor->getDistanceFromRoot()) == std::numeric_limits::max(); + }; - suite.addTest("Bidirectional link (operator&)", [&] { + suite.test("Bidirectional link (operator&)") = [&] { auto node1 = NetworkPtr::makeRoot(logger, "Node1"); auto node2 = NetworkPtr::make(logger, "Node2"); @@ -281,11 +281,11 @@ namespace CppUtils::UnitTest::Container::NetworkPtr auto node1Accessor = node1->sharedAccess(); auto node2Accessor = node2->sharedAccess(); - suite.expectEqual(node1Accessor->getDistanceFromRoot(), 0uz); - suite.expectEqual(node2Accessor->getDistanceFromRoot(), 1uz); - }); + expect(node1Accessor->getDistanceFromRoot()) == 0uz; + expect(node2Accessor->getDistanceFromRoot()) == 1uz; + }; - suite.addTest("Bidirectional unlink (operator/)", [&] { + suite.test("Bidirectional unlink (operator/)") = [&] { auto node1 = NetworkPtr::makeRoot(logger, "Node1"); auto node2 = NetworkPtr::make(logger, "Node2"); node1 & node2; @@ -293,7 +293,7 @@ namespace CppUtils::UnitTest::Container::NetworkPtr node1 / node2; auto node2Accessor = node2->sharedAccess(); - suite.expectEqual(node2Accessor->getDistanceFromRoot(), std::numeric_limits::max()); - }); - }}; + expect(node2Accessor->getDistanceFromRoot()) == std::numeric_limits::max(); + }; + }; } diff --git a/tests/Container/Pair.mpp b/tests/Container/Pair.mpp index d5c0b460..0de0fd17 100644 --- a/tests/Container/Pair.mpp +++ b/tests/Container/Pair.mpp @@ -5,14 +5,14 @@ import CppUtils; namespace CppUtils::UnitTest::Container::Pair { - auto _ = TestSuite{"Container/Pair", {"UnitTest"}, [](TestSuite& suite) { + auto _ = testSuite("Container/Pair").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("Swap pair", [&] { + suite.test("Swap pair") = [&] { auto pair = std::make_pair(42, "Hello World!"); auto newPair = CppUtils::Container::swapPair(pair); - suite.expectEqual(newPair.first, "Hello World!"sv); - suite.expectEqual(newPair.second, 42); - }); - }}; + expect(newPair.first) == "Hello World!"; + expect(newPair.second) == 42; + }; + }; } diff --git a/tests/Container/SafeShared.mpp b/tests/Container/SafeShared.mpp index 411120ca..ceda84d5 100644 --- a/tests/Container/SafeShared.mpp +++ b/tests/Container/SafeShared.mpp @@ -4,60 +4,60 @@ import CppUtils; namespace CppUtils::UnitTest::Container::SafeShared { - static_assert([] { - auto shared = CppUtils::Container::makeSafeShared(42); - if (not shared or shared.use_count() != 1uz) - return false; - - auto copy = shared; - if (shared.use_count() != 2uz) - return false; - - { - auto accessor = shared->uniqueAccess(); - accessor.value() = 100; - } - - return copy->sharedAccess().value() == 100; - }()); - - static_assert([] { - auto shared = CppUtils::Container::makeSafeShared(10); - auto weak = CppUtils::Container::SafeWeak{shared}; - if (weak.expired() or weak.use_count() != 1uz) - return false; - - auto locked = weak.lock(); - return locked and locked.use_count() == 2uz; - }()); - - auto _ = TestSuite{"Container/SafeShared", {"UnitTest", "Thread/SharedLocker"}, [](TestSuite& suite) { - suite.addTest("Empty", [&] { + auto _ = testSuite("Container/SafeShared").dependsOn("UnitTest", "Thread/SharedLocker") = [](TestSuite& suite) { + suite.test<[] { + auto shared = CppUtils::Container::makeSafeShared(42); + if (not shared or shared.use_count() != 1uz) + return false; + + auto copy = shared; + if (shared.use_count() != 2uz) + return false; + + { + auto accessor = shared->uniqueAccess(); + accessor.value() = 100; + } + + return copy->sharedAccess().value() == 100; + }>("Constexpr unique access"); + + suite.test<[] { + auto shared = CppUtils::Container::makeSafeShared(10); + auto weak = CppUtils::Container::SafeWeak{shared}; + if (weak.expired() or weak.use_count() != 1uz) + return false; + + auto locked = weak.lock(); + return locked and locked.use_count() == 2uz; + }>("Constexpr SafeWeak lock"); + + suite.test("Empty") = [&] { auto safeSharedString = CppUtils::Container::SafeShared{}; - suite.expect(not safeSharedString); - suite.expectEqual(safeSharedString.use_count(), 0uz); - suite.expect(safeSharedString == nullptr); - }); + expect(not safeSharedString); + expect(safeSharedString.use_count()) == 0uz; + expect(safeSharedString) == nullptr; + }; - suite.addTest("Unique access", [&] { + suite.test("Unique access") = [&] { auto safeSharedString0 = CppUtils::Container::makeSafeShared("Foo"); - suite.expect(safeSharedString0); + expect(safeSharedString0); auto safeSharedString1 = safeSharedString0; { auto accessor = safeSharedString0->uniqueAccess(); - suite.expectEqual(accessor.value(), "Foo"); + expect(accessor.value()) == "Foo"; accessor.value() = "Bar"; - suite.expectEqual(accessor.value(), "Bar"); + expect(accessor.value()) == "Bar"; } { auto accessor = safeSharedString1->uniqueAccess(); - suite.expectEqual(accessor.value(), "Bar"); + expect(accessor.value()) == "Bar"; } - }); + }; - suite.addTest("Shared access", [&] { + suite.test("Shared access") = [&] { auto safeSharedString0 = CppUtils::Container::makeSafeShared("Foo"); auto safeSharedString1 = safeSharedString0; { @@ -65,73 +65,73 @@ namespace CppUtils::UnitTest::Container::SafeShared auto accessor1 = safeSharedString0->sharedAccess(); auto accessor2 = safeSharedString1->sharedAccess(); auto accessor3 = safeSharedString1->sharedAccess(); - suite.expectEqual(accessor0.value(), "Foo"); - suite.expectEqual(accessor1.value(), "Foo"); - suite.expectEqual(accessor2.value(), "Foo"); - suite.expectEqual(accessor3.value(), "Foo"); + expect(accessor0.value()) == "Foo"; + expect(accessor1.value()) == "Foo"; + expect(accessor2.value()) == "Foo"; + expect(accessor3.value()) == "Foo"; } - }); + }; - suite.addTest("SafeWeak", [&] { + suite.test("SafeWeak") = [&] { auto safeSharedString0 = CppUtils::Container::makeSafeShared("Foo"); auto safeWeak = CppUtils::Container::SafeWeak{safeSharedString0}; - suite.expect(not safeWeak.expired()); + expect(not safeWeak.expired()); auto safeSharedString1 = safeWeak.lock(); { auto accessor0 = safeSharedString0->sharedAccess(); auto accessor1 = safeSharedString1->sharedAccess(); - suite.expectEqual(accessor0.value(), "Foo"); - suite.expectEqual(accessor1.value(), "Foo"); + expect(accessor0.value()) == "Foo"; + expect(accessor1.value()) == "Foo"; } - }); + }; - suite.addTest("SafeWeak expiration and lock failure", [&] { + suite.test("SafeWeak expiration and lock failure") = [&] { auto weak = CppUtils::Container::SafeWeak{}; - suite.expect(weak.expired()); - suite.expectEqual(weak.use_count(), 0uz); + expect(weak.expired()); + expect(weak.use_count()) == 0uz; { auto shared = CppUtils::Container::makeSafeShared("Temporary"); weak = shared; - suite.expect(not weak.expired()); - suite.expectEqual(weak.use_count(), 1uz); + expect(not weak.expired()); + expect(weak.use_count()) == 1uz; } - suite.expect(weak.expired()); - suite.expectEqual(weak.use_count(), 0uz); + expect(weak.expired()); + expect(weak.use_count()) == 0uz; auto locked = weak.lock(); - suite.expect(not locked); - suite.expect(locked == nullptr); - }); + expect(not locked); + expect(locked) == nullptr; + }; - suite.addTest("Move semantics and reset", [&] { + suite.test("Move semantics and reset") = [&] { auto safeShared0 = CppUtils::Container::makeSafeShared(42); - suite.expect(safeShared0); - suite.expectEqual(safeShared0.use_count(), 1uz); + expect(safeShared0); + expect(safeShared0.use_count()) == 1uz; auto safeShared1 = std::move(safeShared0); - suite.expect(not safeShared0); - suite.expect(safeShared1); - suite.expectEqual(safeShared1.use_count(), 1uz); - suite.expectEqual(safeShared1->sharedAccess().value(), 42); + expect(not safeShared0); + expect(safeShared1); + expect(safeShared1.use_count()) == 1uz; + expect(safeShared1->sharedAccess().value()) == 42; safeShared1.reset(); - suite.expect(not safeShared1); - suite.expectEqual(safeShared1.use_count(), 0uz); - }); + expect(not safeShared1); + expect(safeShared1.use_count()) == 0uz; + }; - suite.addTest("Comparison and ownership operators", [&] { + suite.test("Comparison and ownership operators") = [&] { auto safeShared0 = CppUtils::Container::makeSafeShared(10); auto safeShared1 = safeShared0; auto safeShared2 = CppUtils::Container::makeSafeShared(10); - suite.expect(safeShared0 == safeShared1); - suite.expect(not(safeShared0 == safeShared2)); - suite.expect(safeShared0.owner_equal(safeShared1)); - suite.expect(not safeShared0.owner_equal(safeShared2)); - suite.expect(safeShared0 != nullptr); - suite.expect(CppUtils::Container::SafeShared{} == nullptr); - }); - }}; + expect(safeShared0) == safeShared1; + expect(not(safeShared0 == safeShared2)); + expect(safeShared0.owner_equal(safeShared1)); + expect(not safeShared0.owner_equal(safeShared2)); + expect(safeShared0) != nullptr; + expect(CppUtils::Container::SafeShared{}) == nullptr; + }; + }; } diff --git a/tests/Container/Sequence.mpp b/tests/Container/Sequence.mpp index 11622900..f3c2a0b9 100644 --- a/tests/Container/Sequence.mpp +++ b/tests/Container/Sequence.mpp @@ -26,25 +26,46 @@ namespace namespace CppUtils::UnitTest::Container::Sequence { - auto _ = TestSuite{"Container/Sequence", {"UnitTest"}, [](TestSuite& suite) { + auto _ = testSuite("Container/Sequence").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("string", [&] { + suite.test("string") = [] { const auto s0 = "azTESTerABCtyui99o9p"sv; const auto s1 = "000azWXYZerty123uiopHELLO"sv; const auto differences = generateDifferenceVector("+++ ----++++ --- +++ -- - +++++"sv); - const auto result = CppUtils::Container::Sequence::getDifferences(s0, s1); - suite.expectEqual(result, differences); - }); + const auto result = CppUtils::Container::Sequence::getDifferences(s0, s1); + expect(result) == differences; + }; - suite.addTest("vector", [&] { + suite.test("vector") = [] { const auto s0 = "azTESTerABCtyui99o9p"sv; const auto s1 = "000azWXYZerty123uiopHELLO"sv; - const auto vec0 = std::vector{s0.begin(), s0.end()}; - const auto vec1 = std::vector{s1.begin(), s1.end()}; + const auto vec0 = std::vector{std::ranges::begin(s0), std::ranges::end(s0)}; + const auto vec1 = std::vector{std::ranges::begin(s1), std::ranges::end(s1)}; const auto differences = generateDifferenceVector("+++ ----++++ --- +++ -- - +++++"sv); - const auto result = CppUtils::Container::Sequence::getDifferences(vec0, vec1); - suite.expectEqual(result, differences); - }); - }}; + const auto result = CppUtils::Container::Sequence::getDifferences(vec0, vec1); + expect(result) == differences; + }; + + suite.test("mixed range types") = [] { + const auto s0 = "azTESTerABCtyui99o9p"sv; + const auto s1 = "000azWXYZerty123uiopHELLO"sv; + const auto vec1 = std::vector{std::ranges::begin(s1), std::ranges::end(s1)}; + const auto differences = generateDifferenceVector("+++ ----++++ --- +++ -- - +++++"sv); + const auto result = CppUtils::Container::Sequence::getDifferences(s0, vec1); + expect(result) == differences; + }; + + suite.test("custom comparator and projection") = [] { + const auto left = std::vector{1, -2, 3}; + const auto right = std::vector{1, 2, 4}; + const auto result = CppUtils::Container::Sequence::getDifferences( + left, + right, + std::ranges::equal_to{}, + [](int x) { return std::abs(x); }, + [](int x) { return std::abs(x); }); + expect(std::ranges::size(result)) == 4uz; + }; + }; } diff --git a/tests/Container/Stack.mpp b/tests/Container/Stack.mpp index 6b75b01c..28e5ad5b 100644 --- a/tests/Container/Stack.mpp +++ b/tests/Container/Stack.mpp @@ -5,102 +5,102 @@ import CppUtils; namespace CppUtils::UnitTest::Container::Stack { - auto _ = TestSuite{"Container/Stack", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Container/Stack").dependsOn("Logger") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("Empty Stack", [&] { + suite.test("Empty Stack") = [&] { auto stack = CppUtils::Container::Stack{}; - suite.expect(std::ranges::empty(stack)); - }); + expect(std::ranges::empty(stack)); + }; - suite.addTest("Push one value", [&] { + suite.test("Push one value") = [&] { auto stack = CppUtils::Container::Stack{}; stack.push(42); - suite.expect(not std::ranges::empty(stack)); - suite.expectEqual(std::ranges::size(stack), sizeof(int)); - }); + expect(not std::ranges::empty(stack)); + expect(std::ranges::size(stack)) == sizeof(int); + }; - suite.addTest("Pop", [&] { + suite.test("Pop") = [&] { auto stack = CppUtils::Container::Stack{}; stack.push(42); { auto result = stack.pop(); - suite.expect(result); - suite.expectEqual(result.value(), 42); + expect(result); + expect(result.value()) == 42; } - suite.expect(std::ranges::empty(stack)); + expect(std::ranges::empty(stack)); { auto result = stack.pop(); - suite.expect(not result); + expect(not result); } - }); + }; - suite.addTest("One type, multiple values", [&] { + suite.test("One type, multiple values") = [&] { auto stack = CppUtils::Container::Stack{}; stack.push(1); stack.push(2); stack.push(3); - suite.expectEqual(std::ranges::size(stack), sizeof(int) * 3); - }); + expect(std::ranges::size(stack)) == sizeof(int) * 3; + }; - suite.addTest("Multiple types", [&] { + suite.test("Multiple types") = [&] { auto stack = CppUtils::Container::Stack{}; stack.push(true); stack.push(42); stack.push(3.14f); stack.push("Hello World!"sv); - suite.expectEqual(std::ranges::size(stack), sizeof(bool) + sizeof(int) + sizeof(float) + sizeof(std::string_view)); + expect(std::ranges::size(stack)) == sizeof(bool) + sizeof(int) + sizeof(float) + sizeof(std::string_view); stack.dump(); { auto result = stack.get(sizeof(int) + sizeof(float) + sizeof(std::string_view)); - suite.expect(result); + expect(result); Logger::print("{}", result.value()); } { auto result = stack.get(sizeof(float) + sizeof(std::string_view)); - suite.expect(result); + expect(result); Logger::print("{}", result.value()); } { auto result = stack.get(sizeof(std::string_view)); - suite.expect(result); + expect(result); Logger::print("{}", result.value()); } { auto result = stack.get(); - suite.expect(result); + expect(result); Logger::print("{}", result.value()); } - }); + }; - suite.addTest("Call function: () -> std::size_t", [&] { + suite.test("Call function: () -> std::size_t") = [&] { auto stack = CppUtils::Container::Stack{}; stack.push(0uz); auto getNumber = []() -> std::size_t { return 42uz; }; - suite.expect(stack.call(+getNumber)); + expect(stack.call(+getNumber)); { auto result = stack.pop(); - suite.expect(result); - suite.expectEqual(result.value(), 42uz); + expect(result); + expect(result.value()) == 42uz; } - suite.expect(std::ranges::empty(stack)); - }); + expect(std::ranges::empty(stack)); + }; - suite.addTest("Call function: (std::size_t) -> void", [&] { + suite.test("Call function: (std::size_t) -> void") = [&] { auto stack = CppUtils::Container::Stack{}; auto print = [](std::size_t nb) -> void { std::println("{}", nb); }; stack.push(42uz); - suite.expect(stack.call(print)); - }); + expect(stack.call(print)); + }; - suite.addTest("Call function: (std::size_t, std::size_t) -> std::size_t", [&] { + suite.test("Call function: (std::size_t, std::size_t) -> std::size_t") = [&] { auto stack = CppUtils::Container::Stack{}; stack.push(0uz); stack.push(30uz); @@ -108,17 +108,17 @@ namespace CppUtils::UnitTest::Container::Stack auto add = [](std::size_t lhs, std::size_t rhs) -> std::size_t { return lhs + rhs; }; - suite.expect(stack.call(add)); - suite.expect(stack.drop(sizeof(std::size_t) * 2)); + expect(stack.call(add)); + expect(stack.drop(sizeof(std::size_t) * 2)); { auto result = stack.pop(); - suite.expect(result); - suite.expectEqual(result.value(), 42uz); + expect(result); + expect(result.value()) == 42uz; } - suite.expect(std::ranges::empty(stack)); - }); + expect(std::ranges::empty(stack)); + }; - suite.addTest("Call functions", [&] { + suite.test("Call functions") = [&] { auto stack = CppUtils::Container::Stack{}; stack.push(0uz); stack.push(30uz); @@ -129,16 +129,16 @@ namespace CppUtils::UnitTest::Container::Stack auto print = [](std::size_t nb) -> void { std::println("{}", nb); }; - suite.expect(stack.call(add)); - suite.expect(stack.drop(sizeof(std::size_t) * 2)); + expect(stack.call(add)); + expect(stack.drop(sizeof(std::size_t) * 2)); { auto result = stack.get(); - suite.expect(result); - suite.expectEqual(result.value(), 42uz); + expect(result); + expect(result.value()) == 42uz; } - suite.expect(stack.call(print)); - suite.expect(stack.drop(sizeof(std::size_t))); - suite.expect(std::ranges::empty(stack)); - }); - }}; + expect(stack.call(print)); + expect(stack.drop(sizeof(std::size_t))); + expect(std::ranges::empty(stack)); + }; + }; } diff --git a/tests/Container/Tree.mpp b/tests/Container/Tree.mpp index 3ecf2b4f..cc567794 100644 --- a/tests/Container/Tree.mpp +++ b/tests/Container/Tree.mpp @@ -5,27 +5,27 @@ import CppUtils; namespace CppUtils::UnitTest::Container::Tree { - auto _ = TestSuite{"Container/Tree", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Container/Tree").dependsOn("Logger") = [](TestSuite& suite) { using Logger = TestSuite::Logger; using StringTreeNode = CppUtils::Container::Tree::Node; - suite.addTest("", [&] { + suite.test("") = [&] { auto root = StringTreeNode{"Root"}; // CppUtils::Container::Tree::log(root); Logger::print("{}", root.value); - suite.expectEqual(root.value, "Root"); + expect(root.value) == "Root"; root.nodes.emplace_back(StringTreeNode{"Branch0"}); root.nodes.emplace_back(StringTreeNode{"Branch1"}); Logger::print("{}", root.nodes[1].value); - suite.expectEqual(root.nodes[1].value, "Branch1"); + expect(root.nodes[1].value) == "Branch1"; auto& branch0 = root.nodes[0]; branch0.nodes.emplace_back(StringTreeNode{"SubBranch0"}); branch0.nodes[0].nodes.emplace_back(StringTreeNode{"SubSubBranch0"}); branch0.nodes.emplace_back(StringTreeNode{"SubBranch1"}); branch0.nodes.emplace_back(StringTreeNode{"SubBranch2"}); - suite.expectEqual(std::ranges::size(branch0.nodes), 3uz); + expect(std::ranges::size(branch0.nodes)) == 3uz; root.nodes.emplace_back(StringTreeNode{"Branch2"}); auto& branch2 = root.nodes[2]; @@ -36,9 +36,9 @@ namespace CppUtils::UnitTest::Container::Tree root.nodes.emplace_back(StringTreeNode{"Branch3"}); std::print("{}", root); - }); + }; - /*suite.addTest("Differences", [&] { + /*suite.test("Differences") = [&] { [[maybe_unused]] const auto jsonTree0 = R"( { "removed": "value", @@ -58,7 +58,7 @@ namespace CppUtils::UnitTest::Container::Tree } )"_json; // const auto differences = - // suite.expect(jsonTree0 != jsonTree1); - });*/ - }}; + // expect(jsonTree0) != jsonTree1; + };*/ + }; } diff --git a/tests/Container/TypedStack.mpp b/tests/Container/TypedStack.mpp index dffae71d..0dd66898 100644 --- a/tests/Container/TypedStack.mpp +++ b/tests/Container/TypedStack.mpp @@ -5,29 +5,29 @@ import CppUtils; namespace CppUtils::UnitTest::Container::TypedStack { - auto _ = TestSuite{"Container/TypedStack", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Container/TypedStack").dependsOn("Logger") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("Empty Stack", [&] { + suite.test("Empty Stack") = [&] { auto stack = CppUtils::Container::TypedStack{}; - suite.expect(std::ranges::empty(stack)); - }); + expect(std::ranges::empty(stack)); + }; - suite.addTest("One value", [&] { + suite.test("One value") = [&] { auto stack = CppUtils::Container::TypedStack{42}; - suite.expect(!std::ranges::empty(stack)); - suite.expectEqual(std::ranges::size(stack), 1uz); - }); + expect(!std::ranges::empty(stack)); + expect(std::ranges::size(stack)) == 1uz; + }; - suite.addTest("One type, multiple values", [&] { + suite.test("One type, multiple values") = [&] { auto stack = CppUtils::Container::TypedStack{1, 2, 3}; - suite.expectEqual(std::ranges::size(stack), 3uz); - }); + expect(std::ranges::size(stack)) == 3uz; + }; - suite.addTest("Multiple types", [&] { + suite.test("Multiple types") = [&] { auto stack = CppUtils::Container::TypedStack{true, 42, 3.14f, "Hello World!"sv}; - suite.expectEqual(std::ranges::size(stack), 4uz); + expect(std::ranges::size(stack)) == 4uz; stack.print(); @@ -35,43 +35,43 @@ namespace CppUtils::UnitTest::Container::TypedStack Logger::print("{}", stack.get(1)); Logger::print("{}", stack.get(2)); Logger::print("{}", stack.get(3)); - }); + }; - suite.addTest("push/top", [&] { + suite.test("push/top") = [&] { auto stack = CppUtils::Container::TypedStack{}; stack.push(true); - suite.expectEqual(stack.top(), true); + expect(stack.top()); stack.push(42); - suite.expectEqual(stack.top(), 42); + expect(stack.top()) == 42; stack.push(3.14f); - suite.expectEqual(stack.top(), 3.14f); - }); + expect(stack.top()) == 3.14f; + }; - suite.addTest("pushType", [&] { + suite.test("pushType") = [&] { auto stack = CppUtils::Container::TypedStack{}; stack.pushType(1); - suite.expectEqual(stack.top(), 0); - }); + expect(stack.top()) == 0; + }; - suite.addTest("get", [&] { + suite.test("get") = [&] { auto stack = CppUtils::Container::TypedStack{42}; - suite.expectEqual(stack.get(0), 42); - }); + expect(stack.get(0)) == 42; + }; - suite.addTest("pop", [&] { + suite.test("pop") = [&] { auto stack = CppUtils::Container::TypedStack{42}; - suite.expectEqual(stack.pop(), 42); - suite.expect(std::ranges::empty(stack)); - }); + expect(stack.pop()) == 42; + expect(std::ranges::empty(stack)); + }; - suite.addTest("copy", [&] { + suite.test("copy") = [&] { auto stack = CppUtils::Container::TypedStack{0, 42}; stack.copy(1, 0); stack.drop(); - suite.expectEqual(stack.get(0), 42); - }); + expect(stack.get(0)) == 42; + }; - suite.addTest("visit", [&] { + suite.test("visit") = [&] { auto stack = CppUtils::Container::TypedStack{true, 42, 3.14f, "Hello World!"sv}; for (auto i = 0uz; i < std::ranges::size(stack); ++i) @@ -80,9 +80,9 @@ namespace CppUtils::UnitTest::Container::TypedStack Logger::print("{}", value); }); - suite.expectEqual(std::ranges::size(stack), 4uz); - }); + expect(std::ranges::size(stack)) == 4uz; + }; // Todo: tester les autres méthodes - }}; + }; } diff --git a/tests/Container/Vector.mpp b/tests/Container/Vector.mpp index 100770f1..76999d53 100644 --- a/tests/Container/Vector.mpp +++ b/tests/Container/Vector.mpp @@ -5,18 +5,18 @@ import CppUtils; namespace CppUtils::UnitTest::Container::Vector { - auto _ = TestSuite{"Container/Vector", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Container/Vector").dependsOn("Logger") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("merge", [&] { + suite.test("merge") = [&] { const auto a = std::vector{"a", "b"}; const auto b = std::vector{"c", "d"}; const auto c = std::vector{"e", "f"}; const auto d = std::vector{"g", "h"}; const auto vector = CppUtils::Container::Vector::merge(a, b, c, d); - suite.expectEqual(std::ranges::size(vector), 8uz); + expect(std::ranges::size(vector)) == 8uz; for (const auto& string : vector) Logger::print("{}", string); - }); - }}; + }; + }; } diff --git a/tests/Crypto/HMAC.mpp b/tests/Crypto/HMAC.mpp index 189e793b..4c4bcd04 100644 --- a/tests/Crypto/HMAC.mpp +++ b/tests/Crypto/HMAC.mpp @@ -5,15 +5,15 @@ import CppUtils; namespace CppUtils::UnitTest::Crypto::HMAC { - auto _ = TestSuite{"Crypto/HMAC", {"String/Utility", "Crypto/SHA256"}, [](auto& suite) { - suite.addTest("Empty key, empty message", [&] { + auto _ = testSuite("Crypto/HMAC").dependsOn("String/Utility", "Crypto/SHA256") = [](TestSuite& suite) { + suite.test("Empty key, empty message") = [&] { auto hmac = CppUtils::Crypto::HMAC::sign("", ""); - suite.expectEqual(CppUtils::String::toHexString(hmac), "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"); - }); + expect(CppUtils::String::toHexString(hmac)) == "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"; + }; - suite.addTest("Test Case 1", [&] { + suite.test("Test Case 1") = [&] { auto hmac = CppUtils::Crypto::HMAC::sign("key", "The quick brown fox jumps over the lazy dog"); - suite.expectEqual(CppUtils::String::toHexString(hmac), "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"); - }); - }}; + expect(CppUtils::String::toHexString(hmac)) == "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"; + }; + }; } diff --git a/tests/Crypto/SHA256.mpp b/tests/Crypto/SHA256.mpp index cf009492..c46ac144 100644 --- a/tests/Crypto/SHA256.mpp +++ b/tests/Crypto/SHA256.mpp @@ -5,29 +5,29 @@ import CppUtils; namespace CppUtils::UnitTest::Crypto::SHA256 { - auto _ = TestSuite{"Crypto/SHA256", {"String/Utility"}, [](auto& suite) { - suite.addTest("Empty string", [&] { + auto _ = testSuite("Crypto/SHA256").dependsOn("String/Utility") = [](TestSuite& suite) { + suite.test("Empty string") = [&] { const auto hash = CppUtils::Crypto::SHA256::hash(""); - suite.expectEqual(CppUtils::String::toHexString(hash), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); - }); + expect(CppUtils::String::toHexString(hash)) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + }; - suite.addTest("Test vector 1", [&] { + suite.test("Test vector 1") = [&] { const auto hash = CppUtils::Crypto::SHA256::hash("abc"); - suite.expectEqual(CppUtils::String::toHexString(hash), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); - }); + expect(CppUtils::String::toHexString(hash)) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; + }; - suite.addTest("Test vector 2", [&] { + suite.test("Test vector 2") = [&] { const auto hash = CppUtils::Crypto::SHA256::hash("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"); - suite.expectEqual(CppUtils::String::toHexString(hash), "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"); - }); + expect(CppUtils::String::toHexString(hash)) == "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"; + }; - suite.addTest("Multi-step update", [&] { + suite.test("Multi-step update") = [&] { auto sha = CppUtils::Crypto::SHA256{}; sha.update("a"); sha.update("b"); sha.update("c"); const auto hash = sha.digest(); - suite.expectEqual(CppUtils::String::toHexString(hash), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); - }); - }}; + expect(CppUtils::String::toHexString(hash)) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; + }; + }; } diff --git a/tests/Execution/Event.mpp b/tests/Execution/Event.mpp index 87bf4c6d..7cf38172 100644 --- a/tests/Execution/Event.mpp +++ b/tests/Execution/Event.mpp @@ -6,10 +6,10 @@ import CppUtils.Execution.Event; namespace CppUtils::UnitTest::Execution::Event { - auto _ = TestSuite{"Execution/Event", {}, [](auto& suite) { + auto _ = testSuite("Execution/Event") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Wait and Notify", [&] { + suite.test("Wait and Notify") = [&] { auto event = CppUtils::Execution::Event{}; auto threadFinished = std::atomic_flag{}; { @@ -21,10 +21,10 @@ namespace CppUtils::UnitTest::Execution::Event event.wait(); } - suite.expect(threadFinished.test()); - }); + expect(threadFinished.test()); + }; - suite.addTest("WaitFor (Success)", [&] { + suite.test("WaitFor (Success)") = [&] { auto event = CppUtils::Execution::Event{}; { auto thread = std::jthread{[&] { @@ -33,17 +33,17 @@ namespace CppUtils::UnitTest::Execution::Event }}; auto result = event.waitFor(200ms); - suite.expect(result); + expect(result); } - }); + }; - suite.addTest("WaitFor (Timeout)", [&] { + suite.test("WaitFor (Timeout)") = [&] { auto event = CppUtils::Execution::Event{}; auto result = event.waitFor(10ms); - suite.expect(not result); - }); + expect(not result); + }; - suite.addTest("WaitUntil (Success)", [&] { + suite.test("WaitUntil (Success)") = [&] { auto event = CppUtils::Execution::Event{}; { auto thread = std::jthread{[&] { @@ -52,37 +52,37 @@ namespace CppUtils::UnitTest::Execution::Event }}; auto result = event.waitUntil(std::chrono::steady_clock::now() + 200ms); - suite.expect(result); + expect(result); } - }); + }; - suite.addTest("WaitUntil (Timeout)", [&] { + suite.test("WaitUntil (Timeout)") = [&] { auto event = CppUtils::Execution::Event{}; auto result = event.waitUntil(std::chrono::steady_clock::now() + 10ms); - suite.expect(not result); - }); + expect(not result); + }; - suite.addTest("Manual-Reset Behavior (Broadcast)", [&] { + suite.test("Manual-Reset Behavior (Broadcast)") = [&] { auto event = CppUtils::Execution::Event{}; event.notify(); event.wait(); auto result = event.waitFor(10ms); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("Reset Behavior", [&] { + suite.test("Reset Behavior") = [&] { auto event = CppUtils::Execution::Event{}; event.notify(); event.reset(); auto result = event.waitFor(10ms); - suite.expect(not result); - }); + expect(not result); + }; - suite.addTest("Broadcast to Multiple Threads", [&] { + suite.test("Broadcast to Multiple Threads") = [&] { auto event = CppUtils::Execution::Event{}; auto count = std::atomic_size_t{0}; { @@ -92,7 +92,7 @@ namespace CppUtils::UnitTest::Execution::Event std::this_thread::sleep_for(10ms); event.notify(); } - suite.expectEqual(count.load(), 2uz); - }); - }}; + expect(count.load()) == 2uz; + }; + }; } diff --git a/tests/Execution/EventDispatcher.mpp b/tests/Execution/EventDispatcher.mpp index 0cde43a4..5b1a50be 100644 --- a/tests/Execution/EventDispatcher.mpp +++ b/tests/Execution/EventDispatcher.mpp @@ -5,95 +5,94 @@ import CppUtils; namespace CppUtils::UnitTest::Execution::EventDispatcher { - auto _ = TestSuite{ - "Execution/EventDispatcher", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Execution/EventDispatcher").dependsOn("Logger") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::String::Literals; using Logger = TestSuite::Logger; - suite.addTest("Empty", [&] { + suite.test("Empty") = [&] { auto _ = CppUtils::Execution::EventDispatcher{}; - }); + }; - suite.addTest("Subscribe", [&] { + suite.test("Subscribe") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; - eventDispatcher.subscribe<>([&suite](const std::string& message) -> void { + eventDispatcher.subscribe<>([](const std::string& message) -> void { Logger::print("{}", message); - suite.expect(false); + expect(false); }); - }); + }; - suite.addTest("Subscribe with name", [&] { + suite.test("Subscribe with name") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; - eventDispatcher.subscribe<"Ping">([&suite](const std::string& message) -> void { + eventDispatcher.subscribe<"Ping">([](const std::string& message) -> void { Logger::print("{}", message); - suite.expect(false); + expect(false); }); - }); + }; - suite.addTest("Emit without subscribers", [&] { + suite.test("Emit without subscribers") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; eventDispatcher.emit<>("ping"s); eventDispatcher.emit<"Ping">("ping"s); - }); + }; - suite.addTest("Send/Receive", [&] { + suite.test("Send/Receive") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; auto expectPingReceived = std::atomic_bool{false}; - eventDispatcher.subscribe<>([&expectPingReceived, &suite](const std::string& message) -> void { + eventDispatcher.subscribe<>([&expectPingReceived](const std::string& message) -> void { Logger::print("{}", message); expectPingReceived = true; - suite.expectEqual(message, "ping"sv); + expect(message) == "ping"; }); eventDispatcher.emit<>("ping"s); - suite.expect(expectPingReceived); - }); + expect(expectPingReceived); + }; - suite.addTest("Send/Receive with name", [&] { + suite.test("Send/Receive with name") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; auto expectPingReceived = std::atomic_bool{false}; - eventDispatcher.subscribe<"Ping">([&expectPingReceived, &suite](const std::string& message) -> void { + eventDispatcher.subscribe<"Ping">([&expectPingReceived](const std::string& message) -> void { Logger::print("{}", message); expectPingReceived = true; - suite.expectEqual(message, "ping"sv); + expect(message) == "ping"; }); - eventDispatcher.subscribe<"Other">([&suite](const std::string& message) -> void { + eventDispatcher.subscribe<"Other">([](const std::string& message) -> void { Logger::print("{}", message); - suite.expect(false); + expect(false); }); eventDispatcher.emit<"Ping">("ping"s); - suite.expect(expectPingReceived); - }); + expect(expectPingReceived); + }; - suite.addTest("Cascade calls", [&] { + suite.test("Cascade calls") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; auto expectPingReceived = std::atomic_bool{false}; auto expectPongReceived = std::atomic_bool{false}; - eventDispatcher.subscribe<"Ping">([&expectPingReceived, &suite, &eventDispatcher](const std::string& message) -> void { + eventDispatcher.subscribe<"Ping">([&expectPingReceived, &eventDispatcher](const std::string& message) -> void { Logger::print("{}", message); expectPingReceived = true; - suite.expectEqual(message, "ping"sv); + expect(message) == "ping"; eventDispatcher.emit<"Pong">("pong"s); }); - eventDispatcher.subscribe<"Pong">([&expectPongReceived, &suite](const std::string& message) -> void { + eventDispatcher.subscribe<"Pong">([&expectPongReceived](const std::string& message) -> void { Logger::print("{}", message); expectPongReceived = true; - suite.expectEqual(message, "pong"sv); + expect(message) == "pong"; }); eventDispatcher.emit<"Ping">("ping"s); - suite.expect(expectPingReceived); - suite.expect(expectPongReceived); - }); + expect(expectPingReceived); + expect(expectPongReceived); + }; - suite.addTest("Multiple subscribers", [&] { + suite.test("Multiple subscribers") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; auto expect1 = std::atomic_bool{false}; auto expect2 = std::atomic_bool{false}; @@ -106,24 +105,24 @@ namespace CppUtils::UnitTest::Execution::EventDispatcher }); eventDispatcher.emit<"Event">(); - suite.expect(expect1); - suite.expect(expect2); - }); + expect(expect1); + expect(expect2); + }; - suite.addTest("Mutable Lambda", [&] { + suite.test("Mutable Lambda") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; - eventDispatcher.subscribe<"Count">([count = 0, &suite](int expected) mutable { + eventDispatcher.subscribe<"Count">([count = 0](int expected) mutable { ++count; - suite.expectEqual(count, expected); + expect(count) == expected; }); eventDispatcher.emit<"Count">(1); eventDispatcher.emit<"Count">(2); eventDispatcher.emit<"Count">(3); - }); + }; - suite.addTest("Multiple subscribe", [&] { + suite.test("Multiple subscribe") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; auto count = std::atomic_size_t{0}; @@ -136,10 +135,10 @@ namespace CppUtils::UnitTest::Execution::EventDispatcher eventDispatcher.emit<"C">(); eventDispatcher.emit<"D">(); - suite.expectEqual(count, 3uz); - }); + expect(count) == 3uz; + }; - suite.addTest("Unsubscribe", [&] { + suite.test("Unsubscribe") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; auto count = 0uz; @@ -148,15 +147,15 @@ namespace CppUtils::UnitTest::Execution::EventDispatcher }); eventDispatcher.emit<"Event">(); - suite.expectEqual(count, 1uz); + expect(count) == 1uz; - suite.expect(eventDispatcher.unsubscribe(subscriptionIdentifier)); + expect(eventDispatcher.unsubscribe(subscriptionIdentifier)); eventDispatcher.emit<"Event">(); - suite.expectEqual(count, 1uz); - }); + expect(count) == 1uz; + }; - suite.addTest("Clear subscribers", [&] { + suite.test("Clear subscribers") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; auto count = 0uz; @@ -165,16 +164,16 @@ namespace CppUtils::UnitTest::Execution::EventDispatcher eventDispatcher.emit<"A">(); eventDispatcher.emit<"B">(); - suite.expectEqual(count, 2uz); + expect(count) == 2uz; eventDispatcher.clearSubscribers(); eventDispatcher.emit<"A">(); eventDispatcher.emit<"B">(); - suite.expectEqual(count, 2uz); - }); + expect(count) == 2uz; + }; - suite.addTest("Unsubscribe by event hash", [&] { + suite.test("Unsubscribe by event hash") = [&] { auto eventDispatcher = CppUtils::Execution::EventDispatcher{}; auto count = 0uz; @@ -183,20 +182,20 @@ namespace CppUtils::UnitTest::Execution::EventDispatcher eventDispatcher.emit<"Event">(); eventDispatcher.emit<"Other">(); - suite.expectEqual(count, 2uz); + expect(count) == 2uz; - suite.expect(eventDispatcher.unsubscribe<"Event">()); + expect(eventDispatcher.unsubscribe<"Event">()); eventDispatcher.emit<"Event">(); eventDispatcher.emit<"Other">(); - suite.expectEqual(count, 3uz); + expect(count) == 3uz; - suite.expect(not eventDispatcher.unsubscribe<"Event">()); + expect(not eventDispatcher.unsubscribe<"Event">()); - suite.expect(eventDispatcher.unsubscribe("Other"_hash)); + expect(eventDispatcher.unsubscribe("Other"_hash)); eventDispatcher.emit<"Other">(); - suite.expectEqual(count, 3uz); - }); - }}; + expect(count) == 3uz; + }; + }; } diff --git a/tests/Execution/EventQueue.mpp b/tests/Execution/EventQueue.mpp index 4f98c09b..4c67fec1 100644 --- a/tests/Execution/EventQueue.mpp +++ b/tests/Execution/EventQueue.mpp @@ -6,8 +6,8 @@ import CppUtils.UnitTest; namespace CppUtils::UnitTest::Execution::EventQueue { - auto _ = TestSuite{"Execution/EventQueue", {"Execution/EventDispatcher", "Thread/UniqueLocker", "Thread/ThreadLoop", "Execution/ScopeGuard", "UnitTest"}, [](TestSuite& suite) { - suite.addTest("Local EventQueue emission", [&] { + auto _ = testSuite("Execution/EventQueue").dependsOn("Execution/EventDispatcher", "Thread/UniqueLocker", "Thread/ThreadLoop", "Execution/ScopeGuard", "UnitTest") = [](TestSuite& suite) { + suite.test("Local EventQueue emission") = [&] { auto queue = CppUtils::Execution::EventQueue{}; auto received = false; auto valueReceived = 0; @@ -20,11 +20,11 @@ namespace CppUtils::UnitTest::Execution::EventQueue queue.emit<"test">(42); queue.waitUntilFinished(); - suite.expect(received); - suite.expectEqual(valueReceived, 42); - }); + expect(received); + expect(valueReceived) == 42; + }; - suite.addTest("Multiple subscribe", [&] { + suite.test("Multiple subscribe") = [&] { auto queue = CppUtils::Execution::EventQueue{}; auto count = 0uz; @@ -37,10 +37,10 @@ namespace CppUtils::UnitTest::Execution::EventQueue queue.emit<"C">(); queue.waitUntilFinished(); - suite.expectEqual(count, 2uz); - }); + expect(count) == 2uz; + }; - suite.addTest("Unsubscribe", [&] { + suite.test("Unsubscribe") = [&] { auto queue = CppUtils::Execution::EventQueue{}; auto count = 0; @@ -50,16 +50,16 @@ namespace CppUtils::UnitTest::Execution::EventQueue queue.emit<"Event">(); queue.waitUntilFinished(); - suite.expectEqual(count, 1); + expect(count) == 1; - suite.expect(queue.unsubscribe(subscriptionIdentifier)); + expect(queue.unsubscribe(subscriptionIdentifier)); queue.emit<"Event">(); queue.waitUntilFinished(); - suite.expectEqual(count, 1); - }); + expect(count) == 1; + }; - suite.addTest("Clear subscribers", [&] { + suite.test("Clear subscribers") = [&] { auto queue = CppUtils::Execution::EventQueue{}; auto count = 0; @@ -69,17 +69,17 @@ namespace CppUtils::UnitTest::Execution::EventQueue queue.emit<"A">(); queue.emit<"B">(); queue.waitUntilFinished(); - suite.expectEqual(count, 2); + expect(count) == 2; queue.clearSubscribers(); queue.emit<"A">(); queue.emit<"B">(); queue.waitUntilFinished(); - suite.expectEqual(count, 2); - }); + expect(count) == 2; + }; - suite.addTest("Unsubscribe by event hash", [&] { + suite.test("Unsubscribe by event hash") = [&] { auto queue = CppUtils::Execution::EventQueue{}; auto count = 0; @@ -89,14 +89,14 @@ namespace CppUtils::UnitTest::Execution::EventQueue queue.emit<"Event">(); queue.emit<"Other">(); queue.waitUntilFinished(); - suite.expectEqual(count, 2); + expect(count) == 2; - suite.expect(queue.unsubscribe<"Event">()); + expect(queue.unsubscribe<"Event">()); queue.emit<"Event">(); queue.emit<"Other">(); queue.waitUntilFinished(); - suite.expectEqual(count, 3); - }); - }}; + expect(count) == 3; + }; + }; } diff --git a/tests/Execution/Pipeline.mpp b/tests/Execution/Pipeline.mpp index afb5c3df..669e1e62 100644 --- a/tests/Execution/Pipeline.mpp +++ b/tests/Execution/Pipeline.mpp @@ -5,8 +5,8 @@ import CppUtils; export namespace CppUtils::UnitTest::Execution { - auto _ = CppUtils::UnitTest::TestSuite{"Execution/Pipeline", [](auto& suite) { - suite.addTest("Pipeline short-circuit", [&] { + auto _ = testSuite("Execution/Pipeline") = [](TestSuite& suite) { + suite.test("Pipeline short-circuit") = [&] { auto pipeline = CppUtils::Execution::Pipeline{}; auto executedStepsCount = 0uz; @@ -23,30 +23,30 @@ export namespace CppUtils::UnitTest::Execution auto _ = pipeline.invoke(failure, executedStepsCount); auto _ = pipeline.invoke(success, executedStepsCount); - suite.expect(executedStepsCount == 2uz); - suite.expect(not pipeline.success()); - suite.expect(result == 1); + expect(executedStepsCount) == 2uz; + expect(not pipeline.success()); + expect(result) == 1; const auto finalResult = pipeline.result(); - suite.expect(not finalResult); - suite.expect(finalResult.error() == "error 42"); - }); + expect(not finalResult); + expect(finalResult.error()) == "error 42"; + }; - suite.addTest("Pipeline strictly expected", [&] { + suite.test("Pipeline strictly expected") = [&] { auto pipeline = CppUtils::Execution::Pipeline{}; auto success = []() -> std::expected { return 10; }; auto failure = []() -> std::expected { return std::unexpected{"error"}; }; const auto result = pipeline.invoke(success); - suite.expect(result == 10); - suite.expect(pipeline.success()); + expect(result) == 10; + expect(pipeline.success()); auto _ = pipeline.invoke(failure); - suite.expect(not pipeline.success()); - }); + expect(not pipeline.success()); + }; - suite.addTest("Pipeline with lambdas and state capture", [&] { + suite.test("Pipeline with lambdas and state capture") = [&] { auto pipeline = CppUtils::Execution::Pipeline{}; auto executedStepsCount = 0uz; @@ -60,15 +60,15 @@ export namespace CppUtils::UnitTest::Execution return std::to_string(value); }, result1); - suite.expect(pipeline.success()); - suite.expect(pipeline); - suite.expect(executedStepsCount == 2uz); - suite.expect(result1 == 30); - suite.expect(result2 == "30"); - suite.expect(not pipeline.error().has_value()); - }); + expect(pipeline.success()); + expect(pipeline); + expect(executedStepsCount) == 2uz; + expect(result1) == 30; + expect(result2) == "30"; + expect(not pipeline.error().has_value()); + }; - suite.addTest("Pipeline with void return", [&] { + suite.test("Pipeline with void return") = [&] { auto pipeline = CppUtils::Execution::Pipeline{}; auto executedStepsCount = 0uz; @@ -87,12 +87,12 @@ export namespace CppUtils::UnitTest::Execution return {}; }); - suite.expect(executedStepsCount == 2uz); - suite.expect(not pipeline.success()); - suite.expect(pipeline.error().value() == "action failed"); - }); + expect(executedStepsCount) == 2uz; + expect(not pipeline.success()); + expect(pipeline.error().value()) == "action failed"; + }; - suite.addTest("Pipeline tryInvoke with non-default-constructible type", [&] { + suite.test("Pipeline tryInvoke with non-default-constructible type") = [&] { struct NonDefaultConstructible final { int value; @@ -105,47 +105,47 @@ export namespace CppUtils::UnitTest::Execution return NonDefaultConstructible{42}; }); - suite.expect(result1.has_value()); - suite.expect(result1->value == 42); + expect(result1.has_value()); + expect(result1->value) == 42; const auto result2 = pipeline.tryInvoke([]() -> std::expected { return std::unexpected{"failed creation"}; }); - suite.expect(not result2.has_value()); - suite.expect(not pipeline.success()); + expect(not result2.has_value()); + expect(not pipeline.success()); const auto result3 = pipeline.tryInvoke([]() -> std::expected { return NonDefaultConstructible{99}; }); - suite.expect(not result3.has_value()); - }); + expect(not result3.has_value()); + }; - suite.addTest("Pipeline reset and reuse", [&] { + suite.test("Pipeline reset and reuse") = [&] { auto pipeline = CppUtils::Execution::Pipeline{}; pipeline.invoke([]() -> std::expected { return std::unexpected{"initial failure"}; }); - suite.expect(not pipeline.success()); - suite.expect(pipeline.error().value() == "initial failure"); + expect(not pipeline.success()); + expect(pipeline.error().value()) == "initial failure"; pipeline.reset(); - suite.expect(pipeline.success()); - suite.expect(not pipeline.error().has_value()); + expect(pipeline.success()); + expect(not pipeline.error().has_value()); const auto result = pipeline.invoke([]() -> std::expected { return 100; }); - suite.expect(pipeline.success()); - suite.expect(result == 100); - }); + expect(pipeline.success()); + expect(result) == 100; + }; - suite.addTest("Pipeline with tryExecute interoperability", [&] { + suite.test("Pipeline with tryExecute interoperability") = [&] { auto pipeline = CppUtils::Execution::Pipeline{}; const auto result = pipeline.invoke([] { @@ -154,19 +154,19 @@ export namespace CppUtils::UnitTest::Execution }); }); - suite.expect(pipeline.success()); - suite.expect(result == 84); - }); + expect(pipeline.success()); + expect(result) == 84; + }; - suite.addTest("Pipeline result with value and move semantics", [&] { + suite.test("Pipeline result with value and move semantics") = [&] { auto successfulPipeline = CppUtils::Execution::Pipeline{}; const auto computationResult = successfulPipeline.invoke([]() -> std::expected { return 42; }); const auto finalSuccessResult = std::move(successfulPipeline).result(computationResult); - suite.expect(finalSuccessResult.has_value()); - suite.expect(finalSuccessResult.value() == 42); + expect(finalSuccessResult.has_value()); + expect(finalSuccessResult.value()) == 42; auto failedPipeline = CppUtils::Execution::Pipeline{}; failedPipeline.invoke([]() -> std::expected { @@ -174,8 +174,8 @@ export namespace CppUtils::UnitTest::Execution }); const auto finalFailedResult = std::move(failedPipeline).result(100); - suite.expect(not finalFailedResult.has_value()); - suite.expect(finalFailedResult.error() == "pipeline failed"); - }); - }}; + expect(not finalFailedResult.has_value()); + expect(finalFailedResult.error()) == "pipeline failed"; + }; + }; } diff --git a/tests/Execution/Retry.mpp b/tests/Execution/Retry.mpp index 9b932f57..7c7a0951 100644 --- a/tests/Execution/Retry.mpp +++ b/tests/Execution/Retry.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Execution::Retry { - auto _ = TestSuite{"Execution/Retry", {}, [](TestSuite& suite) { + auto _ = testSuite("Execution/Retry") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("Immediate success", [&] { + suite.test("Immediate success") = [&] { auto attemptsCount = 0uz; const auto result = CppUtils::Execution::retry( @@ -18,12 +18,12 @@ namespace CppUtils::UnitTest::Execution::Retry return 42; }); - suite.expectEqual(attemptsCount, 1uz); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), 42); - }); + expect(attemptsCount) == 1uz; + expect(result.has_value()); + expect(result.value()) == 42; + }; - suite.addTest("Success after retries", [&] { + suite.test("Success after retries") = [&] { auto attemptsCount = 0uz; struct FailureReport final { @@ -44,17 +44,17 @@ namespace CppUtils::UnitTest::Execution::Retry capturedErrors.push_back({.attempt = attempt, .error = error}); }); - suite.expectEqual(attemptsCount, 3uz); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), "success"); - suite.expectEqual(std::ranges::size(capturedErrors), 2uz); - suite.expectEqual(capturedErrors[0].attempt, 1uz); - suite.expectEqual(capturedErrors[0].error, "temporary failure 1"); - suite.expectEqual(capturedErrors[1].attempt, 2uz); - suite.expectEqual(capturedErrors[1].error, "temporary failure 2"); - }); - - suite.addTest("All attempts failed", [&] { + expect(attemptsCount) == 3uz; + expect(result.has_value()); + expect(result.value()) == "success"; + expect(std::ranges::size(capturedErrors)) == 2uz; + expect(capturedErrors[0].attempt) == 1uz; + expect(capturedErrors[0].error) == "temporary failure 1"; + expect(capturedErrors[1].attempt) == 2uz; + expect(capturedErrors[1].error) == "temporary failure 2"; + }; + + suite.test("All attempts failed") = [&] { auto attemptsCount = 0uz; auto failureReportsCount = 0uz; @@ -65,16 +65,16 @@ namespace CppUtils::UnitTest::Execution::Retry }, [&](std::size_t attempt, const std::string&) { ++failureReportsCount; - suite.expectEqual(attempt, attemptsCount); + expect(attempt) == attemptsCount; }); - suite.expectEqual(attemptsCount, 3uz); - suite.expectEqual(failureReportsCount, 3uz); - suite.expect(not result.has_value()); - suite.expectEqual(result.error(), "fatal error 3"); - }); + expect(attemptsCount) == 3uz; + expect(failureReportsCount) == 3uz; + expect(not result.has_value()); + expect(result.error()) == "fatal error 3"; + }; - suite.addTest("Boolean operation", [&] { + suite.test("Boolean operation") = [&] { auto attemptsCount = 0uz; const auto success = CppUtils::Execution::retry( @@ -84,11 +84,11 @@ namespace CppUtils::UnitTest::Execution::Retry return attemptsCount >= 2; }); - suite.expectEqual(attemptsCount, 2uz); - suite.expect(success); - }); + expect(attemptsCount) == 2uz; + expect(success); + }; - suite.addTest("Throwing operation", [&] { + suite.test("Throwing operation") = [&] { auto attemptsCount = 0uz; struct FailureReport final { @@ -110,15 +110,15 @@ namespace CppUtils::UnitTest::Execution::Retry capturedExceptions.push_back({.attempt = attempt, .error = error}); }); - suite.expectEqual(attemptsCount, 2uz); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), 42); - suite.expectEqual(std::ranges::size(capturedExceptions), 1uz); - suite.expectEqual(capturedExceptions[0].attempt, 1uz); - suite.expectEqual(capturedExceptions[0].error, "temporary exception"s); - }); + expect(attemptsCount) == 2uz; + expect(result.has_value()); + expect(result.value()) == 42; + expect(std::ranges::size(capturedExceptions)) == 1uz; + expect(capturedExceptions[0].attempt) == 1uz; + expect(capturedExceptions[0].error) == "temporary exception"; + }; - suite.addTest("Delay between attempts", [&] { + suite.test("Delay between attempts") = [&] { auto attemptsCount = 0uz; const auto startTime = std::chrono::steady_clock::now(); @@ -135,10 +135,10 @@ namespace CppUtils::UnitTest::Execution::Retry }); const auto elapsedTime = std::chrono::duration_cast(std::chrono::steady_clock::now() - startTime); - suite.expectEqual(attemptsCount, 3uz); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), 42); - suite.expect(elapsedTime >= 10ms); - }); - }}; + expect(attemptsCount) == 3uz; + expect(result.has_value()); + expect(result.value()) == 42; + expect(elapsedTime) >= 10ms; + }; + }; } diff --git a/tests/Execution/ScopeGuard.mpp b/tests/Execution/ScopeGuard.mpp index 1cc88c18..23d4e209 100644 --- a/tests/Execution/ScopeGuard.mpp +++ b/tests/Execution/ScopeGuard.mpp @@ -5,18 +5,18 @@ import CppUtils; namespace CppUtils::UnitTest::Execution::ScopeGuard { - auto _ = TestSuite{"Execution/ScopeGuard", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("Basic ScopeGuard", [&] { + auto _ = testSuite("Execution/ScopeGuard").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test("Basic ScopeGuard") = [&] { auto called = false; { auto _ = CppUtils::Execution::ScopeGuard{[&called]() { called = true; }}; } - suite.expect(called); - }); + expect(called); + }; - suite.addTest("ScopeGuard with Exception", [&] { + suite.test("ScopeGuard with Exception") = [&] { auto called = false; try { @@ -27,7 +27,7 @@ namespace CppUtils::UnitTest::Execution::ScopeGuard } catch (const std::runtime_error&) {} - suite.expect(called); - }); - }}; + expect(called); + }; + }; } diff --git a/tests/Execution/TryExecute.mpp b/tests/Execution/TryExecute.mpp index 1b3caede..0e3cd1e2 100644 --- a/tests/Execution/TryExecute.mpp +++ b/tests/Execution/TryExecute.mpp @@ -5,44 +5,44 @@ import CppUtils; namespace CppUtils::UnitTest::Execution::TryExecute { - auto _ = TestSuite{"Execution/TryExecute", {}, [](TestSuite& suite) { + auto _ = testSuite("Execution/TryExecute") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("Function returning value", [&] { + suite.test("Function returning value") = [&] { const auto result = CppUtils::Execution::tryExecute([] { return 42; }); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), 42); - }); + expect(result.has_value()); + expect(result.value()) == 42; + }; - suite.addTest("Function returning void", [&] { + suite.test("Function returning void") = [&] { auto executed = false; const auto result = CppUtils::Execution::tryExecute([&] { executed = true; }); - suite.expect(executed); - suite.expect(result.has_value()); - }); + expect(executed); + expect(result.has_value()); + }; - suite.addTest("Function throwing std::exception", [&] { + suite.test("Function throwing std::exception") = [&] { const auto result = CppUtils::Execution::tryExecute([] -> int { throw std::runtime_error{"disk read failed"}; }); - suite.expect(not result.has_value()); - suite.expectEqual(result.error(), "disk read failed"s); - }); + expect(not result.has_value()); + expect(result.error()) == "disk read failed"; + }; - suite.addTest("Function throwing non-std exception", [&] { + suite.test("Function throwing non-std exception") = [&] { const auto result = CppUtils::Execution::tryExecute([] -> int { throw 123; }); - suite.expect(not result.has_value()); - suite.expectEqual(result.error(), "Unknown exception"s); - }); - }}; + expect(not result.has_value()); + expect(result.error()) == "Unknown exception"; + }; + }; } diff --git a/tests/FileSystem/Directory.mpp b/tests/FileSystem/Directory.mpp index ff2f57c6..fb13ddb6 100644 --- a/tests/FileSystem/Directory.mpp +++ b/tests/FileSystem/Directory.mpp @@ -5,17 +5,17 @@ import CppUtils; namespace CppUtils::UnitTest::FileSystem::Directory { - auto _ = TestSuite{"FileSystem/Directory", {"UnitTest", "Logger"}, [](TestSuite& suite) { + auto _ = testSuite("FileSystem/Directory").dependsOn("UnitTest", "Logger") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("TemporaryDirectory", [&] { + suite.test("TemporaryDirectory") = [&] { auto functionCalled = false; - CppUtils::FileSystem::TemporaryDirectory{[&functionCalled, &suite](const auto& directory) -> void { + CppUtils::FileSystem::TemporaryDirectory{[&functionCalled](const auto& directory) -> void { functionCalled = true; - suite.expect(std::filesystem::exists(directory)); + expect(std::filesystem::exists(directory)); Logger::print("{}", directory.string()); }}; - suite.expect(functionCalled); - }); - }}; + expect(functionCalled); + }; + }; } diff --git a/tests/FileSystem/File.mpp b/tests/FileSystem/File.mpp index 5d2c43e5..080f06ff 100644 --- a/tests/FileSystem/File.mpp +++ b/tests/FileSystem/File.mpp @@ -5,12 +5,12 @@ import CppUtils; namespace CppUtils::UnitTest::FileSystem::File { - auto _ = TestSuite{"FileSystem/File", {"Logger", "FileSystem/Directory"}, [](TestSuite& suite) { + auto _ = testSuite("FileSystem/File").dependsOn("Logger", "FileSystem/Directory") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("forFiles", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("forFiles") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { CppUtils::FileSystem::String::write(directory / "file.txt", "test"); CppUtils::FileSystem::String::write(directory / "file.log", "test"); auto filesCounter = 0u; @@ -18,12 +18,12 @@ namespace CppUtils::UnitTest::FileSystem::File Logger::print("{}", filePath.string()); ++filesCounter; }); - suite.expectEqual(filesCounter, 2u); + expect(filesCounter) == 2u; }}; - }); + }; - suite.addTest("forFilesWithExtension", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("forFilesWithExtension") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { CppUtils::FileSystem::String::write(directory / "file.txt", "test"); CppUtils::FileSystem::String::write(directory / "file.log", "test"); auto filesCounter = 0u; @@ -31,12 +31,12 @@ namespace CppUtils::UnitTest::FileSystem::File Logger::print("{}", filePath.string()); ++filesCounter; }); - suite.expectEqual(filesCounter, 1u); + expect(filesCounter) == 1u; }}; - }); + }; - suite.addTest("Binary/readByBlocks", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Binary/readByBlocks") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { const auto filePath = directory / "test.tmp"; auto originalData = std::vector{std::byte{0xDE}, std::byte{0xAD}, std::byte{0xBE}, std::byte{0xEF}}; CppUtils::FileSystem::Binary::writeVector(filePath, originalData); @@ -49,14 +49,14 @@ namespace CppUtils::UnitTest::FileSystem::File ++nbBlocks; }, blockSize); - suite.expect(result); - suite.expectEqual(resultData, originalData); - suite.expectEqual(nbBlocks, (std::ranges::size(originalData) + blockSize - 1) / blockSize); + expect(result); + expect(resultData == originalData); + expect(nbBlocks) == (std::ranges::size(originalData) + blockSize - 1) / blockSize; }}; - }); + }; - suite.addTest("String/readByBlocks", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("String/readByBlocks") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { const auto filePath = directory / "test.tmp"; constexpr auto originalString = "Hello World!"sv; CppUtils::FileSystem::String::write(filePath, originalString); @@ -69,21 +69,21 @@ namespace CppUtils::UnitTest::FileSystem::File ++nbBlocks; }, blockSize); - suite.expect(result); - suite.expectEqual(resultString, originalString); - suite.expectEqual(nbBlocks, (std::ranges::size(originalString) + blockSize - 1) / blockSize); + expect(result); + expect(resultString) == originalString; + expect(nbBlocks) == (std::ranges::size(originalString) + blockSize - 1) / blockSize; }}; - }); + }; - suite.addTest("readByBlocks (error)", [&] { + suite.test("readByBlocks (error)") = [&] { const auto result = CppUtils::FileSystem::Binary::readByBlocks("non_existent_file.tmp", [](auto) {}); - suite.expect(not result); - suite.expect(result.error().type == CppUtils::FileSystem::Error::Type::OpenFailed); - suite.expectEqual(result.error().message, "Failed to open file"sv); - }); + expect(not result); + expect(result.error().type) == CppUtils::FileSystem::Error::Type::OpenFailed; + expect(result.error().message) == "Failed to open file"; + }; - suite.addTest("readByBlocks (callback error propagation)", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("readByBlocks (callback error propagation)") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { const auto filePath = directory / "test.tmp"; CppUtils::FileSystem::String::write(filePath, "Some content"); @@ -91,14 +91,14 @@ namespace CppUtils::UnitTest::FileSystem::File return std::unexpected{"Error from callback"}; }); - suite.expect(not result); - suite.expect(result.error().type == CppUtils::FileSystem::Error::Type::CallbackError); - suite.expectEqual(result.error().message, "Error from callback"sv); + expect(not result); + expect(result.error().type) == CppUtils::FileSystem::Error::Type::CallbackError; + expect(result.error().message) == "Error from callback"; }}; - }); + }; - suite.addTest("IncrementalHash", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("IncrementalHash") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { const auto filePath = directory / "test.tmp"; constexpr auto originalString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."sv; CppUtils::FileSystem::String::write(filePath, originalString); @@ -108,32 +108,32 @@ namespace CppUtils::UnitTest::FileSystem::File finalHash = CppUtils::Type::hash(block, finalHash); }, 8uz); - suite.expect(result); + expect(result); auto expectedHash = CppUtils::Type::hash(originalString); - suite.expectEqual(finalHash, expectedHash); + expect(finalHash) == expectedHash; }}; - }); - }}; + }; + }; namespace Binary { - auto _ = TestSuite{"FileSystem/File/Binary", {"Logger", "FileSystem/Directory"}, [](TestSuite& suite) { + auto _ = testSuite("FileSystem/File/Binary").dependsOn("Logger", "FileSystem/Directory") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("WriteRead", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("WriteRead") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto filePath = directory / "test.tmp"; auto originalValue = 12'345; CppUtils::FileSystem::Binary::write(filePath, originalValue); auto fileContent = CppUtils::FileSystem::Binary::read(filePath); Logger::print("{}", fileContent); - suite.expectEqual(fileContent, originalValue); + expect(fileContent) == originalValue; }}; - }); + }; - suite.addTest("WriteReadVector", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("WriteReadVector") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto filePath = directory / "test.tmp"; auto originalVector = std::vector{}; originalVector.resize(10); @@ -142,30 +142,30 @@ namespace CppUtils::UnitTest::FileSystem::File auto vector = CppUtils::FileSystem::Binary::readVector(filePath); for (auto nb : vector) Logger::print("{}", nb); - suite.expectEqual(vector, originalVector); + expect(vector) == originalVector; }}; - }); - }}; + }; + }; } namespace String { - auto _ = TestSuite{"FileSystem/File/String", {"Logger", "FileSystem/Directory"}, [](TestSuite& suite) { + auto _ = testSuite("FileSystem/File/String").dependsOn("Logger", "FileSystem/Directory") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("WriteRead", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("WriteRead") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto filePath = directory / "test.tmp"; auto originalString = "Hello World!"; CppUtils::FileSystem::String::write(filePath, originalString); auto fileContent = CppUtils::FileSystem::String::read(filePath); Logger::print("{}", fileContent); - suite.expectEqual(fileContent, originalString); + expect(fileContent) == originalString; }}; - }); + }; - suite.addTest("Append", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Append") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto filePath = directory / "test.tmp"; auto firstString = "Hello "; auto secondString = "World!"; @@ -173,9 +173,9 @@ namespace CppUtils::UnitTest::FileSystem::File CppUtils::FileSystem::String::append(filePath, secondString); auto fileContent = CppUtils::FileSystem::String::read(filePath); Logger::print("{}", fileContent); - suite.expectEqual(fileContent, std::string{firstString} + secondString); + expect(fileContent) == std::string{firstString} + secondString; }}; - }); - }}; + }; + }; } } diff --git a/tests/FileSystem/FileRange.mpp b/tests/FileSystem/FileRange.mpp index fdd3fa24..4ef9d83e 100644 --- a/tests/FileSystem/FileRange.mpp +++ b/tests/FileSystem/FileRange.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::FileSystem::FileRange { - auto _ = TestSuite{"FileSystem/FileRange", {"FileSystem/File", "FileSystem/Directory"}, [](TestSuite& suite) { + auto _ = testSuite("FileSystem/FileRange").dependsOn("FileSystem/File", "FileSystem/Directory") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("readLines", [&] { + suite.test("readLines") = [&] { CppUtils::FileSystem::TemporaryDirectory{[&](const auto& directory) { const auto filePath = directory / "lines.txt"; CppUtils::FileSystem::String::write(filePath, "Line 1\nLine 2\nLine 3"); @@ -17,14 +17,14 @@ namespace CppUtils::UnitTest::FileSystem::FileRange for (const auto& line : CppUtils::FileSystem::readLines(filePath)) lines.push_back(line); - suite.expectEqual(std::ranges::size(lines), 3uz); - suite.expectEqual(lines[0], "Line 1"s); - suite.expectEqual(lines[1], "Line 2"s); - suite.expectEqual(lines[2], "Line 3"s); + expect(std::ranges::size(lines)) == 3uz; + expect(lines[0]) == "Line 1"; + expect(lines[1]) == "Line 2"; + expect(lines[2]) == "Line 3"; }}; - }); + }; - suite.addTest("readLines with CRLF", [&] { + suite.test("readLines with CRLF") = [&] { CppUtils::FileSystem::TemporaryDirectory{[&](const auto& directory) { const auto filePath = directory / "crlf.txt"; CppUtils::FileSystem::String::write(filePath, "Line 1\r\nLine 2\r\nLine 3\r\n"); @@ -33,24 +33,24 @@ namespace CppUtils::UnitTest::FileSystem::FileRange for (const auto& line : CppUtils::FileSystem::readLines(filePath)) lines.push_back(line); - suite.expectEqual(std::ranges::size(lines), 3uz); - suite.expectEqual(lines[0], "Line 1"s); - suite.expectEqual(lines[1], "Line 2"s); - suite.expectEqual(lines[2], "Line 3"s); + expect(std::ranges::size(lines)) == 3uz; + expect(lines[0]) == "Line 1"; + expect(lines[1]) == "Line 2"; + expect(lines[2]) == "Line 3"; }}; - }); + }; - suite.addTest("readLines empty file", [&] { + suite.test("readLines empty file") = [&] { CppUtils::FileSystem::TemporaryDirectory{[&](const auto& directory) { const auto filePath = directory / "empty.txt"; CppUtils::FileSystem::String::write(filePath, ""); const auto reader = CppUtils::FileSystem::readLines(filePath); - suite.expect(std::ranges::cbegin(reader) == std::ranges::cend(reader)); + expect(std::ranges::cbegin(reader)) == std::ranges::cend(reader); }}; - }); + }; - suite.addTest("writeLines and pipeline", [&] { + suite.test("writeLines and pipeline") = [&] { CppUtils::FileSystem::TemporaryDirectory{[&](const auto& directory) { const auto sourceFilePath = directory / "source.txt"; const auto destinationFilePath = directory / "destination.txt"; @@ -59,13 +59,13 @@ namespace CppUtils::UnitTest::FileSystem::FileRange CppUtils::FileSystem::readLines(sourceFilePath) | std::views::transform([](std::string_view line) { - return std::format("[{}]", line); - }) + return std::format("[{}]", line); + }) | CppUtils::FileSystem::writeLines(destinationFilePath); const auto fileContent = CppUtils::FileSystem::String::read(destinationFilePath); - suite.expectEqual(fileContent, "[Line 1]\n[Line 2]\n[Line 3]\n"s); + expect(fileContent) == "[Line 1]\n[Line 2]\n[Line 3]\n"; }}; - }); - }}; + }; + }; } diff --git a/tests/FileSystem/FileStaging.mpp b/tests/FileSystem/FileStaging.mpp index d624558c..e8aa917e 100644 --- a/tests/FileSystem/FileStaging.mpp +++ b/tests/FileSystem/FileStaging.mpp @@ -5,9 +5,9 @@ import CppUtils; namespace CppUtils::UnitTest::FileSystem::FileStaging { - auto _ = TestSuite{"FileSystem/FileStaging", {"FileSystem/Directory"}, [](TestSuite& suite) { - suite.addTest("FileStaging stage and complete with archive", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& sandbox) -> void { + auto _ = testSuite("FileSystem/FileStaging").dependsOn("FileSystem/Directory") = [](TestSuite& suite) { + suite.test("FileStaging stage and complete with archive") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& sandbox) -> void { const auto inputDirectory = sandbox / "input"; const auto stagingDirectory = sandbox / "staging"; const auto archiveDirectory = sandbox / "archive"; @@ -23,25 +23,25 @@ namespace CppUtils::UnitTest::FileSystem::FileStaging quarantineDirectory, archiveDirectory); - suite.expect(stageResult.has_value()); + expect(stageResult.has_value()); const auto& staging = stageResult.value(); - suite.expectEqual(staging.inputPath(), inputFilePath); - suite.expect(not std::filesystem::exists(inputFilePath)); - suite.expect(std::filesystem::exists(staging.stagedPath())); + expect(staging.inputPath()) == inputFilePath; + expect(not std::filesystem::exists(inputFilePath)); + expect(std::filesystem::exists(staging.stagedPath())); auto completeResult = staging.complete(); - suite.expect(completeResult.has_value()); + expect(completeResult.has_value()); const auto& archivedPath = completeResult.value(); - suite.expect(not std::filesystem::exists(staging.stagedPath())); - suite.expect(std::filesystem::exists(archivedPath)); - suite.expectEqual(archivedPath.parent_path(), archiveDirectory); + expect(not std::filesystem::exists(staging.stagedPath())); + expect(std::filesystem::exists(archivedPath)); + expect(archivedPath.parent_path()) == archiveDirectory; }}; - }); + }; - suite.addTest("FileStaging stage and complete with deletion", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& sandbox) -> void { + suite.test("FileStaging stage and complete with deletion") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& sandbox) -> void { const auto inputDirectory = sandbox / "input"; const auto stagingDirectory = sandbox / "staging"; @@ -53,20 +53,20 @@ namespace CppUtils::UnitTest::FileSystem::FileStaging inputFilePath, stagingDirectory); - suite.expect(stageResult.has_value()); + expect(stageResult.has_value()); const auto& staging = stageResult.value(); - suite.expect(std::filesystem::exists(staging.stagedPath())); + expect(std::filesystem::exists(staging.stagedPath())); auto completeResult = staging.complete(); - suite.expect(completeResult.has_value()); - suite.expectEqual(completeResult.value().string(), ""); - suite.expect(not std::filesystem::exists(staging.stagedPath())); + expect(completeResult.has_value()); + expect(completeResult.value().string()) == ""; + expect(not std::filesystem::exists(staging.stagedPath())); }}; - }); + }; - suite.addTest("FileStaging stage and abort with quarantine", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& sandbox) -> void { + suite.test("FileStaging stage and abort with quarantine") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& sandbox) -> void { const auto inputDirectory = sandbox / "input"; const auto stagingDirectory = sandbox / "staging"; const auto archiveDirectory = sandbox / "archive"; @@ -82,21 +82,21 @@ namespace CppUtils::UnitTest::FileSystem::FileStaging quarantineDirectory, archiveDirectory); - suite.expect(stageResult.has_value()); + expect(stageResult.has_value()); const auto& staging = stageResult.value(); auto abortResult = staging.abort(); - suite.expect(abortResult.has_value()); + expect(abortResult.has_value()); const auto& quarantinedPath = abortResult.value(); - suite.expect(not std::filesystem::exists(staging.stagedPath())); - suite.expect(std::filesystem::exists(quarantinedPath)); - suite.expectEqual(quarantinedPath.parent_path(), quarantineDirectory); + expect(not std::filesystem::exists(staging.stagedPath())); + expect(std::filesystem::exists(quarantinedPath)); + expect(quarantinedPath.parent_path()) == quarantineDirectory; }}; - }); + }; - suite.addTest("FileStaging stage and abort with deletion", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& sandbox) -> void { + suite.test("FileStaging stage and abort with deletion") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& sandbox) -> void { const auto inputDirectory = sandbox / "input"; const auto stagingDirectory = sandbox / "staging"; @@ -106,23 +106,22 @@ namespace CppUtils::UnitTest::FileSystem::FileStaging auto stageResult = CppUtils::FileSystem::FileStaging::stage( inputFilePath, - stagingDirectory - ); + stagingDirectory); - suite.expect(stageResult.has_value()); + expect(stageResult.has_value()); const auto& staging = stageResult.value(); auto abortResult = staging.abort(); - suite.expect(abortResult.has_value()); - suite.expectEqual(abortResult.value().string(), ""); + expect(abortResult.has_value()); + expect(abortResult.value().string()) == ""; - suite.expect(not std::filesystem::exists(staging.stagedPath())); - suite.expect(not std::filesystem::exists(inputFilePath)); + expect(not std::filesystem::exists(staging.stagedPath())); + expect(not std::filesystem::exists(inputFilePath)); }}; - }); + }; - suite.addTest("FileStaging stage and retry", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& sandbox) -> void { + suite.test("FileStaging stage and retry") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& sandbox) -> void { const auto inputDirectory = sandbox / "input"; const auto stagingDirectory = sandbox / "staging"; @@ -134,25 +133,25 @@ namespace CppUtils::UnitTest::FileSystem::FileStaging inputFilePath, stagingDirectory); - suite.expect(stageResult.has_value()); + expect(stageResult.has_value()); const auto& staging = stageResult.value(); auto retryResult = staging.retry(); - suite.expect(retryResult.has_value()); - suite.expectEqual(retryResult.value(), inputFilePath); + expect(retryResult.has_value()); + expect(retryResult.value()) == inputFilePath; - suite.expect(not std::filesystem::exists(staging.stagedPath())); - suite.expect(std::filesystem::exists(inputFilePath)); + expect(not std::filesystem::exists(staging.stagedPath())); + expect(std::filesystem::exists(inputFilePath)); }}; - }); + }; - suite.addTest("FileStaging non-existent file error", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& sandbox) -> void { + suite.test("FileStaging non-existent file error") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& sandbox) -> void { const auto inputFilePath = sandbox / "does_not_exist.csv"; auto stageResult = CppUtils::FileSystem::FileStaging::stage(inputFilePath); - suite.expect(not stageResult.has_value()); + expect(not stageResult.has_value()); }}; - }); - }}; + }; + }; } diff --git a/tests/FileSystem/IndexedStorage.mpp b/tests/FileSystem/IndexedStorage.mpp index 8af6c5b5..6a93ec0d 100644 --- a/tests/FileSystem/IndexedStorage.mpp +++ b/tests/FileSystem/IndexedStorage.mpp @@ -6,11 +6,11 @@ import CppUtils.FileSystem.IndexedStorage; namespace CppUtils::UnitTest::FileSystem::IndexedStorage { - auto _ = TestSuite{"FileSystem/IndexedStorage", [](TestSuite& suite) { + auto _ = testSuite("FileSystem/IndexedStorage") = [](TestSuite& suite) { using namespace CppUtils::Memory::Literals; - suite.addTest("Store and Load", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + suite.test("Store and Load") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { auto storage = CppUtils::FileSystem::IndexedStorage{testDirectory, 1_KiB}; storage.store(1, "Hello World"); @@ -19,40 +19,40 @@ namespace CppUtils::UnitTest::FileSystem::IndexedStorage auto value1 = storage.load(1); auto value2 = storage.load(2); - suite.expect(value1.has_value()); - suite.expect(value1.value() == "Hello World"); - suite.expect(value2.has_value()); - suite.expect(value2.value() == "foobar"); - suite.expect(storage.contains(1)); - suite.expect(storage.contains(2)); - suite.expect(not storage.contains(3)); + expect(value1.has_value()); + expect(value1.value()) == "Hello World"; + expect(value2.has_value()); + expect(value2.value()) == "foobar"; + expect(storage.contains(1)); + expect(storage.contains(2)); + expect(not storage.contains(3)); }}; - }); + }; - suite.addTest("Remove and Recycle", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + suite.test("Remove and Recycle") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { auto storage = CppUtils::FileSystem::IndexedStorage{testDirectory}; storage.store(1, 100); storage.store(2, 200); const auto chunkPath = testDirectory / "chunk_0.bin"; - suite.expect(std::filesystem::file_size(chunkPath) == 2 * sizeof(int)); + expect(std::filesystem::file_size(chunkPath)) == 2 * sizeof(int); - suite.expect(storage.contains(1)); + expect(storage.contains(1)); storage.remove(1); - suite.expect(not storage.contains(1)); + expect(not storage.contains(1)); storage.store(3, 300); auto value3 = storage.load(3); - suite.expect(value3.has_value()); - suite.expect(value3.value() == 300); + expect(value3.has_value()); + expect(value3.value()) == 300; - suite.expect(std::filesystem::file_size(chunkPath) == 2 * sizeof(int)); + expect(std::filesystem::file_size(chunkPath)) == 2 * sizeof(int); }}; - }); + }; - suite.addTest("Best-Fit Recycling", [&suite] { + suite.test("Best-Fit Recycling") = [] { CppUtils::FileSystem::TemporaryDirectory{[&](const auto& testDirectory) { auto storage = CppUtils::FileSystem::IndexedStorage{testDirectory}; @@ -74,15 +74,15 @@ namespace CppUtils::UnitTest::FileSystem::IndexedStorage storage.store(4, "Fits in Med"); - suite.expect(storage.contains(4)); + expect(storage.contains(4)); { auto indexAccessor = storage.getIndex(); - suite.expect(indexAccessor->at(4).offset == targetOffset); + expect(indexAccessor->at(4).offset) == targetOffset; } }}; - }); + }; - suite.addTest("Thread Safety", [&suite] { + suite.test("Thread Safety") = [] { CppUtils::FileSystem::TemporaryDirectory{[&](const auto& testDirectory) { auto storage = CppUtils::FileSystem::IndexedStorage{testDirectory}; @@ -104,12 +104,12 @@ namespace CppUtils::UnitTest::FileSystem::IndexedStorage }); } threads.clear(); - suite.expect(success.load()); + expect(success.load()); }}; - }); + }; - suite.addTest("Persistence", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + suite.test("Persistence") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { { auto storage = CppUtils::FileSystem::IndexedStorage{testDirectory}; storage.store(10, 420); @@ -121,16 +121,16 @@ namespace CppUtils::UnitTest::FileSystem::IndexedStorage auto value1 = storage.load(10); auto value2 = storage.load(20); - suite.expect(value1.has_value()); - suite.expect(value1.value() == 420); - suite.expect(value2.has_value()); - suite.expect(value2.value() == "Persisted String"); + expect(value1.has_value()); + expect(value1.value()) == 420; + expect(value2.has_value()); + expect(value2.value()) == "Persisted String"; } }}; - }); + }; - suite.addTest("Multiple Chunks", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + suite.test("Multiple Chunks") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { auto storage = CppUtils::FileSystem::IndexedStorage{testDirectory, 128_B}; for (auto i = 0uz; i < 100uz; ++i) @@ -139,18 +139,18 @@ namespace CppUtils::UnitTest::FileSystem::IndexedStorage const auto chunkCount = std::ranges::count_if(std::filesystem::directory_iterator{testDirectory}, [](const auto& entry) { return entry.path().filename().string().starts_with("chunk_"); }); - suite.expect(chunkCount > 1); + expect(chunkCount) > 1; for (auto i = 0uz; i < 100uz; ++i) { auto value = storage.load(i); - suite.expect(value.has_value()); - suite.expect(value.value() == static_cast(i * 42)); + expect(value.has_value()); + expect(value.value()) == static_cast(i * 42); } }}; - }); + }; - suite.addTest("Coalescing Free Blocks", [&suite] { + suite.test("Coalescing Free Blocks") = [] { CppUtils::FileSystem::TemporaryDirectory{[&](const auto& testDirectory) { auto storage = CppUtils::FileSystem::IndexedStorage{testDirectory}; @@ -160,7 +160,7 @@ namespace CppUtils::UnitTest::FileSystem::IndexedStorage const auto chunkPath = testDirectory / "chunk_0.bin"; const auto sizeBefore = std::filesystem::file_size(chunkPath); - suite.expect(sizeBefore == 3 * sizeof(int)); + expect(sizeBefore) == 3 * sizeof(int); storage.remove(1); storage.remove(2); @@ -169,12 +169,12 @@ namespace CppUtils::UnitTest::FileSystem::IndexedStorage storage.store>(4, emptyVector); auto sizeAfter = std::filesystem::file_size(chunkPath); - suite.expect(sizeAfter == sizeBefore); + expect(sizeAfter) == sizeBefore; auto loadedVector = storage.load>(4); - suite.expect(loadedVector.has_value()); - suite.expect(std::ranges::empty(loadedVector.value())); + expect(loadedVector.has_value()); + expect(std::ranges::empty(loadedVector.value())); }}; - }); - }}; + }; + }; } diff --git a/tests/FileSystem/MeshPager.mpp b/tests/FileSystem/MeshPager.mpp index fc0ec99f..cd639dfa 100644 --- a/tests/FileSystem/MeshPager.mpp +++ b/tests/FileSystem/MeshPager.mpp @@ -61,9 +61,9 @@ namespace CppUtils::Type::Binary namespace CppUtils::UnitTest::FileSystem::MeshPager { - auto _ = TestSuite{"FileSystem/MeshPager", [](TestSuite& suite) { - suite.addTest("Paging and RAII logic", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + auto _ = testSuite("FileSystem/MeshPager") = [](TestSuite& suite) { + suite.test("Paging and RAII logic") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { using MeshPager = CppUtils::FileSystem::MeshPager; using MeshNode = MeshPager::MeshNodePtr; @@ -83,15 +83,15 @@ namespace CppUtils::UnitTest::FileSystem::MeshPager return std::make_pair(pager.track(nodeC), nodeA); }(); - suite.expectEqual(SpyValue::activeCount, 3uz); + expect(SpyValue::activeCount) == 3uz; { auto focus = pager.acquireFocus(nodeA, 1); - suite.expectEqual(SpyValue::activeCount, 2uz); + expect(SpyValue::activeCount) == 2uz; auto nodeB = nodeA["link"].front().value(); auto focusB = pager.acquireFocus(nodeB, 1); - suite.expectEqual(SpyValue::activeCount, 3uz); + expect(SpyValue::activeCount) == 3uz; auto nodeC = MeshNode{}; for (auto neighbor : nodeB["link"]) @@ -101,16 +101,16 @@ namespace CppUtils::UnitTest::FileSystem::MeshPager break; } - suite.expect(nodeC); - suite.expectEqual(nodeC.getValue().value().value, "NodeC"); + expect(nodeC); + expect(nodeC.getValue().value().value) == "NodeC"; } - suite.expectEqual(SpyValue::activeCount, 1uz); + expect(SpyValue::activeCount) == 1uz; }}; - }); + }; - suite.addTest("Automatic node deletion", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + suite.test("Automatic node deletion") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { using MeshPager = CppUtils::FileSystem::MeshPager; using MeshNode = MeshPager::MeshNodePtr; @@ -132,13 +132,13 @@ namespace CppUtils::UnitTest::FileSystem::MeshPager { auto pager = MeshPager{testDirectory}; auto loadedB = pager.loadNode(idB); - suite.expect(not loadedB); + expect(not loadedB); } }}; - }); + }; - suite.addTest("Automatic sub-graph deletion", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + suite.test("Automatic sub-graph deletion") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { using MeshPager = CppUtils::FileSystem::MeshPager; using MeshNode = MeshPager::MeshNodePtr; @@ -166,14 +166,14 @@ namespace CppUtils::UnitTest::FileSystem::MeshPager auto pager = MeshPager{testDirectory}; auto loadedB = pager.loadNode(idB); auto loadedC = pager.loadNode(idC); - suite.expect(not loadedB); - suite.expect(not loadedC); + expect(not loadedB); + expect(not loadedC); } }}; - }); + }; - suite.addTest("Automatic disk sub-graph deletion", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + suite.test("Automatic disk sub-graph deletion") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { using MeshPager = CppUtils::FileSystem::MeshPager; using MeshNode = MeshPager::MeshNodePtr; @@ -212,14 +212,14 @@ namespace CppUtils::UnitTest::FileSystem::MeshPager auto pager = MeshPager{testDirectory}; auto loadedB = pager.loadNode(idB); auto loadedC = pager.loadNode(idC); - suite.expect(not loadedB); - suite.expect(not loadedC); + expect(not loadedB); + expect(not loadedC); } }}; - }); + }; - suite.addTest("Root node persistence", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + suite.test("Root node persistence") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { using MeshPager = CppUtils::FileSystem::MeshPager; using MeshNode = MeshPager::MeshNodePtr; @@ -245,16 +245,16 @@ namespace CppUtils::UnitTest::FileSystem::MeshPager auto pager = MeshPager{testDirectory}; auto loadedA = pager.loadNode(idA); auto loadedB = pager.loadNode(idB); - suite.expect(loadedA); - suite.expectEqual(loadedA.getValue().value().value, "NodeA"); - suite.expect(loadedB); - suite.expectEqual(loadedB.getValue().value().value, "NodeB"); + expect(loadedA); + expect(loadedA.getValue().value().value) == "NodeA"; + expect(loadedB); + expect(loadedB.getValue().value().value) == "NodeB"; } }}; - }); + }; - suite.addTest("Shared child node persistence", [&suite] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& testDirectory) { + suite.test("Shared child node persistence") = [] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& testDirectory) { using MeshPager = CppUtils::FileSystem::MeshPager; using MeshNode = MeshPager::MeshNodePtr; @@ -281,10 +281,10 @@ namespace CppUtils::UnitTest::FileSystem::MeshPager { auto pager = MeshPager{testDirectory}; auto loadedC = pager.loadNode(idC); - suite.expect(loadedC); - suite.expectEqual(loadedC.getValue().value().value, "NodeC"); + expect(loadedC); + expect(loadedC.getValue().value().value) == "NodeC"; } }}; - }); - }}; + }; + }; } diff --git a/tests/FileSystem/Watcher.mpp b/tests/FileSystem/Watcher.mpp index 9b06d85b..919d8028 100644 --- a/tests/FileSystem/Watcher.mpp +++ b/tests/FileSystem/Watcher.mpp @@ -9,29 +9,26 @@ import CppUtils; namespace CppUtils::UnitTest::FileSystem::Watcher { - auto _ = TestSuite{ - "FileSystem/Watcher", - {"Logger", "FileSystem/Directory", "FileSystem/File"}, - [](TestSuite& suite) { + auto _ = testSuite("FileSystem/Watcher").dependsOn("Logger", "FileSystem/Directory", "FileSystem/File") = [](TestSuite& suite) { using namespace std::chrono_literals; using Logger = TestSuite::Logger; - suite.addTest("No file", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("No file") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto watcher = CppUtils::FileSystem::Watcher{}; watcher.watch(directory / "test.tmp"); watcher.onEvent( - [&suite]( + []( [[maybe_unused]] CppUtils::FileSystem::Event event, [[maybe_unused]] const std::filesystem::path& filePath) -> void { - suite.expect(false); + expect(false); }); std::this_thread::sleep_for(10ms); }}; - }); + }; - suite.addTest("No event", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("No event") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto filePath = directory / "test.tmp"; CppUtils::FileSystem::String::write(filePath, "Hello World!"); std::this_thread::sleep_for(100ms); @@ -39,25 +36,25 @@ namespace CppUtils::UnitTest::FileSystem::Watcher auto watcher = CppUtils::FileSystem::Watcher{}; watcher.watch(filePath); watcher.onEvent( - [&suite]( + []( [[maybe_unused]] CppUtils::FileSystem::Event event, [[maybe_unused]] const std::filesystem::path& filePath) -> void { - suite.expect(false); + expect(false); }); std::this_thread::sleep_for(100ms); }}; - }); + }; - suite.addTest("Unwatch", [&] { + suite.test("Unwatch") = [&] { CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto watcher = CppUtils::FileSystem::Watcher{}; watcher.watch(directory); watcher.unwatch(directory); }}; - }); + }; - suite.addTest("Watch file creation", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Watch file creation") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto expectCreateFile = std::atomic_bool{false}; auto expectCloseFile = std::atomic_bool{false}; auto filePath = directory / "test.tmp"; @@ -70,29 +67,29 @@ namespace CppUtils::UnitTest::FileSystem::Watcher Logger::print("{}: {}", event, filePath.string()); }); watcher.onEvent(CppUtils::FileSystem::Event::Created, - [&suite, &expectCreateFile, &filePath]( + [&expectCreateFile, &filePath]( CppUtils::FileSystem::Event event, const std::filesystem::path& eventFilePath) -> void { - suite.expectEqual(event, CppUtils::FileSystem::Event::Created); - suite.expectEqual(eventFilePath, filePath); + expect(event) == CppUtils::FileSystem::Event::Created; + expect(eventFilePath) == filePath; expectCreateFile = true; }); watcher.onEvent(CppUtils::FileSystem::Event::CloseWrite, - [&suite, &expectCloseFile, &filePath]( + [&expectCloseFile, &filePath]( CppUtils::FileSystem::Event event, const std::filesystem::path& eventFilePath) -> void { - suite.expectEqual(event, CppUtils::FileSystem::Event::CloseWrite); - suite.expectEqual(eventFilePath, filePath); + expect(event) == CppUtils::FileSystem::Event::CloseWrite; + expect(eventFilePath) == filePath; expectCloseFile = true; }); CppUtils::FileSystem::String::write(directory / "test.tmp", "Hello World!"); - suite.expectWait([&] { return expectCreateFile and expectCloseFile; }); + expect([&] { return expectCreateFile and expectCloseFile; }) == eventually; }}; - }); + }; - suite.addTest("Watch file modification", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Watch file modification") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto expectModifyFile = std::atomic_bool{false}; auto filePath = directory / "test.tmp"; CppUtils::FileSystem::String::write(filePath, "Foo"); @@ -106,21 +103,21 @@ namespace CppUtils::UnitTest::FileSystem::Watcher Logger::print("{}: {}", event, filePath.string()); }); watcher.onEvent(CppUtils::FileSystem::Event::CloseWrite, - [&suite, &expectModifyFile, &filePath]( + [&expectModifyFile, &filePath]( CppUtils::FileSystem::Event event, const std::filesystem::path& eventFilePath) -> void { - suite.expectEqual(event, CppUtils::FileSystem::Event::CloseWrite); - suite.expectEqual(eventFilePath, filePath); + expect(event) == CppUtils::FileSystem::Event::CloseWrite; + expect(eventFilePath) == filePath; expectModifyFile = true; }); CppUtils::FileSystem::String::write(filePath, "Bar"); - suite.expectWait([&] { return expectModifyFile.load(); }); + expect([&] { return expectModifyFile.load(); }) == eventually; }}; - }); + }; - suite.addTest("Watch file modification in a directory", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Watch file modification in a directory") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto expectModifyFile = std::atomic_bool{false}; auto filePath = directory / "test.tmp"; CppUtils::FileSystem::String::write(filePath, "Foo"); @@ -134,21 +131,21 @@ namespace CppUtils::UnitTest::FileSystem::Watcher Logger::print("{}: {}", event, filePath.string()); }); watcher.onEvent(CppUtils::FileSystem::Event::CloseWrite, - [&suite, &expectModifyFile, &filePath]( + [&expectModifyFile, &filePath]( CppUtils::FileSystem::Event event, const std::filesystem::path& eventFilePath) -> void { - suite.expectEqual(event, CppUtils::FileSystem::Event::CloseWrite); - suite.expectEqual(eventFilePath, filePath); + expect(event) == CppUtils::FileSystem::Event::CloseWrite; + expect(eventFilePath) == filePath; expectModifyFile = true; }); CppUtils::FileSystem::String::write(filePath, "Bar"); - suite.expectWait([&] { return expectModifyFile.load(); }); + expect([&] { return expectModifyFile.load(); }) == eventually; }}; - }); + }; - suite.addTest("Watch file deletion", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Watch file deletion") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto expectDeleteFile = std::atomic_bool{false}; auto filePath = directory / "test.tmp"; CppUtils::FileSystem::String::write(filePath, "Hello World!"); @@ -162,64 +159,64 @@ namespace CppUtils::UnitTest::FileSystem::Watcher Logger::print("{}: {}", event, filePath.string()); }); watcher.onEvent(CppUtils::FileSystem::Event::Deleted, - [&suite, &expectDeleteFile, &filePath]( + [&expectDeleteFile, &filePath]( CppUtils::FileSystem::Event event, const std::filesystem::path& eventFilePath) -> void { - suite.expectEqual(event, CppUtils::FileSystem::Event::Deleted); - suite.expectEqual(eventFilePath, filePath); + expect(event) == CppUtils::FileSystem::Event::Deleted; + expect(eventFilePath) == filePath; expectDeleteFile = true; }); std::filesystem::remove(filePath); - suite.expectWait([&] { return expectDeleteFile.load(); }); + expect([&] { return expectDeleteFile.load(); }) == eventually; }}; - }); + }; - suite.addTest("Passive start and stop", [&] { + suite.test("Passive start and stop") = [&] { auto watcher = CppUtils::FileSystem::Watcher{}; - suite.expect(not watcher.isRunning()); + expect(not watcher.isRunning()); watcher.start(); - suite.expect(watcher.isRunning()); + expect(watcher.isRunning()); watcher.stop(); - suite.expect(not watcher.isRunning()); + expect(not watcher.isRunning()); watcher.start(); - suite.expect(watcher.isRunning()); + expect(watcher.isRunning()); watcher.stop(); - suite.expect(not watcher.isRunning()); - }); + expect(not watcher.isRunning()); + }; - suite.addTest("Reset watcher", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Reset watcher") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto watcher = CppUtils::FileSystem::Watcher{}; - suite.expect(not watcher.isRunning()); + expect(not watcher.isRunning()); watcher.watch(directory); - suite.expect(watcher.isRunning()); + expect(watcher.isRunning()); watcher.reset(); - suite.expect(not watcher.isRunning()); + expect(not watcher.isRunning()); }}; - }); + }; - suite.addTest("Move watcher", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Move watcher") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { auto watcher1 = CppUtils::FileSystem::Watcher{}; - suite.expect(not watcher1.isRunning()); + expect(not watcher1.isRunning()); watcher1.watch(directory); - suite.expect(watcher1.isRunning()); + expect(watcher1.isRunning()); auto watcher2 = std::move(watcher1); - suite.expect(watcher2.isRunning()); - suite.expect(not watcher1.isRunning()); + expect(watcher2.isRunning()); + expect(not watcher1.isRunning()); watcher2.stop(); - suite.expect(not watcher2.isRunning()); + expect(not watcher2.isRunning()); }}; - }); - }}; + }; + }; } diff --git a/tests/Language/ASTParser.mpp b/tests/Language/ASTParser.mpp index 9a621617..bee76634 100644 --- a/tests/Language/ASTParser.mpp +++ b/tests/Language/ASTParser.mpp @@ -17,31 +17,27 @@ namespace CppUtils::UnitTest::Language::ASTParser }); // clang-format on }; - - static_assert([] { - using namespace std::literals; - using namespace CppUtils::String::Literals; - - auto interpreter = CppUtils::Language::ASTParser{}; - addStack(interpreter); - constexpr auto source = "parent"sv; - auto cursor = CppUtils::String::Cursor{source}; - auto result = interpreter(cursor); - if (not result) - return false; - const auto& scopeNodes = interpreter.getScope().nodes; - return std::ranges::size(scopeNodes) == 1uz and scopeNodes.front().value == "parent"_token; - }()); - - auto _ = TestSuite{ - "Language/ASTParser", - {"Logger", "Container/Tree", "Language/MetaCircularVirtualMachine"}, - [](TestSuite& suite) { + auto _ = testSuite("Language/ASTParser").dependsOn("Logger", "Container/Tree", "Language/MetaCircularVirtualMachine") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::String::Literals; using Logger = TestSuite::Logger; - suite.addTest("Empty source", [&] { + suite.test<[] { + using namespace std::literals; + using namespace CppUtils::String::Literals; + + auto interpreter = CppUtils::Language::ASTParser{}; + addStack(interpreter); + constexpr auto source = "token"sv; + auto cursor = CppUtils::String::Cursor{source}; + auto result = interpreter(cursor); + if (not result) + return false; + const auto& scopeNodes = interpreter.getScope().nodes; + return std::ranges::size(scopeNodes) == 1uz and scopeNodes.front().value == "token"_token; + }>("Constexpr token parsing"); + + suite.test("Empty source") = [&] { auto interpreter = CppUtils::Language::ASTParser{}; addStack(interpreter); constexpr auto source = ""sv; @@ -51,12 +47,12 @@ namespace CppUtils::UnitTest::Language::ASTParser if (not result) Logger::print<"error">("Error: {}\nPosition: {}\nChar: '{}'", result.error(), cursor.position, cursor.getCurrent()); - suite.expect(result); + expect(result); Logger::print("{}", interpreter.getScope()); - suite.expect(std::ranges::empty(interpreter.getScope().nodes)); - }); + expect(std::ranges::empty(interpreter.getScope().nodes)); + }; - suite.addTest("Whitespaces", [&] { + suite.test("Whitespaces") = [&] { auto interpreter = CppUtils::Language::ASTParser{}; addStack(interpreter); constexpr auto source = R"( @@ -68,30 +64,30 @@ namespace CppUtils::UnitTest::Language::ASTParser if (not result) Logger::print<"error">("Error: {}\nPosition: {}\nChar: '{}'", result.error(), cursor.position, cursor.getCurrent()); - suite.expect(result); + expect(result); Logger::print("{}", interpreter.getScope()); - suite.expect(std::ranges::empty(interpreter.getScope().nodes)); - }); + expect(std::ranges::empty(interpreter.getScope().nodes)); + }; - suite.addTest("Parse token", [&] { + suite.test("Parse token") = [&] { auto interpreter = CppUtils::Language::ASTParser{}; addStack(interpreter); - constexpr auto source = "parent"sv; + constexpr auto source = "token"sv; auto cursor = CppUtils::String::Cursor{source}; auto result = interpreter(cursor); if (not result) Logger::print<"error">("Error: {}\nPosition: {}\nChar: '{}'", result.error(), cursor.position, cursor.getCurrent()); - suite.expect(result); + expect(result); Logger::print("{}", interpreter.getScope()); const auto& scopeNodes = interpreter.getScope().nodes; - suite.expectEqual(std::ranges::size(scopeNodes), 1uz); - suite.expectEqual(scopeNodes.front().value, "parent"_token); - suite.expectEqual(std::ranges::size(scopeNodes.front().nodes), 0uz); - }); + expect(std::ranges::size(scopeNodes)) == 1uz; + expect(scopeNodes.front().value) == "token"_token; + expect(std::ranges::size(scopeNodes.front().nodes)) == 0uz; + }; - suite.addTest("Empty node", [&] { + suite.test("Empty node") = [&] { auto interpreter = CppUtils::Language::ASTParser{}; addStack(interpreter); constexpr auto source = "parent{}"sv; @@ -101,20 +97,20 @@ namespace CppUtils::UnitTest::Language::ASTParser if (not result) Logger::print<"error">("Error: {}\nPosition: {}\nChar: '{}'", result.error(), cursor.position, cursor.getCurrent()); - suite.expect(result); + expect(result); Logger::print("{}", interpreter.getScope()); const auto& scopeNodes = interpreter.getScope().nodes; - suite.expectEqual(std::ranges::size(scopeNodes), 1uz); - suite.expectEqual(scopeNodes.front().value, "parent"_token); - suite.expectEqual(std::ranges::size(scopeNodes.front().nodes), 0uz); - }); + expect(std::ranges::size(scopeNodes)) == 1uz; + expect(scopeNodes.front().value) == "parent"_token; + expect(std::ranges::size(scopeNodes.front().nodes)) == 0uz; + }; - suite.addTest("Escaped char", [&] { + suite.test("Escaped char") = [&] { // Todo [[maybe_unused]] constexpr auto source = R"(Hello\ World{})"sv; - }); + }; - suite.addTest("Parse value", [&] { + suite.test("Parse value") = [&] { auto interpreter = CppUtils::Language::ASTParser{}; addStack(interpreter); constexpr auto source = R"( @@ -126,14 +122,14 @@ namespace CppUtils::UnitTest::Language::ASTParser if (not result) Logger::print<"error">("Error: {}\nPosition: {}\nChar: '{}'", result.error(), cursor.position, cursor.getCurrent()); - suite.expect(result); + expect(result); Logger::print("{}", interpreter.getScope()); const auto& scopeNodes = interpreter.getScope().nodes; - suite.expectEqual(std::ranges::size(scopeNodes), 1uz); - suite.expectEqual(scopeNodes.front().value, static_cast('a')); - }); + expect(std::ranges::size(scopeNodes)) == 1uz; + expect(scopeNodes.front().value) == static_cast('a'); + }; - suite.addTest("Nested nodes", [&] { + suite.test("Nested nodes") = [&] { auto interpreter = CppUtils::Language::ASTParser{}; addStack(interpreter); constexpr auto source = R"( @@ -148,17 +144,17 @@ namespace CppUtils::UnitTest::Language::ASTParser if (not result) Logger::print<"error">("Error: {}\nPosition: {}\nChar: '{}'", result.error(), cursor.position, cursor.getCurrent()); - suite.expect(result); + expect(result); Logger::print("{}", interpreter.getScope()); const auto& scopeNodes = interpreter.getScope().nodes; - suite.expectEqual(std::ranges::size(scopeNodes), 1uz); - suite.expectEqual(scopeNodes.front().value, "parent"_token); - suite.expectEqual(std::ranges::size(scopeNodes.front().nodes), 2uz); - suite.expectEqual(scopeNodes.front().nodes.at(0).value, "child1"_token); - suite.expectEqual(scopeNodes.front().nodes.at(1).value, "child2"_token); - }); - - suite.addTest("Run function", [&] { + expect(std::ranges::size(scopeNodes)) == 1uz; + expect(scopeNodes.front().value) == "parent"_token; + expect(std::ranges::size(scopeNodes.front().nodes)) == 2uz; + expect(scopeNodes.front().nodes.at(0).value) == "child1"_token; + expect(scopeNodes.front().nodes.at(1).value) == "child2"_token; + }; + + suite.test("Run function") = [&] { auto interpreter = CppUtils::Language::ASTParser{}; addStack(interpreter); constexpr auto source = R"( @@ -172,15 +168,15 @@ namespace CppUtils::UnitTest::Language::ASTParser if (not result) Logger::print<"error">("Error: {}\nPosition: {}\nChar: '{}'", result.error(), cursor.position, cursor.getCurrent()); - suite.expect(result); + expect(result); Logger::print("{}", interpreter.getScope()); const auto& scopeNodes = interpreter.getScope().nodes; - suite.expectEqual(std::ranges::size(scopeNodes), 2uz); - suite.expectEqual(scopeNodes.at(0).value, "run"_token); - suite.expectEqual(scopeNodes.at(1).value, "createNode"_token); - }); + expect(std::ranges::size(scopeNodes)) == 2uz; + expect(scopeNodes.at(0).value) == "run"_token; + expect(scopeNodes.at(1).value) == "createNode"_token; + }; - suite.addTest("Run temporary function", [&] { + suite.test("Run temporary function") = [&] { auto interpreter = CppUtils::Language::ASTParser{}; addStack(interpreter); constexpr auto source = R"( @@ -195,11 +191,11 @@ namespace CppUtils::UnitTest::Language::ASTParser if (not result) Logger::print<"error">("Error: {}\nPosition: {}\nChar: '{}'", result.error(), cursor.position, cursor.getCurrent()); - suite.expect(result); + expect(result); Logger::print("{}", interpreter.getScope()); const auto& scopeNodes = interpreter.getScope().nodes; - suite.expectEqual(std::ranges::size(scopeNodes), 1uz); - suite.expectEqual(scopeNodes.front().value, "createNode"_token); - }); - }}; + expect(std::ranges::size(scopeNodes)) == 1uz; + expect(scopeNodes.front().value) == "createNode"_token; + }; + }; } diff --git a/tests/Language/CLikeCompiler.mpp b/tests/Language/CLikeCompiler.mpp index d059e762..6760bccf 100644 --- a/tests/Language/CLikeCompiler.mpp +++ b/tests/Language/CLikeCompiler.mpp @@ -6,16 +6,13 @@ import CppUtils; namespace CppUtils::UnitTest::Language::CLikeCompiler { /* - auto _ = TestSuite{ - "Language/CLikeCompiler", - {"Logger", "Language/ASTParser"}, - [](auto& suite) { + auto _ = testSuite("Language/CLikeCompiler").dependsOn("Logger", "Language/ASTParser") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; namespace VM = CppUtils::Language::VirtualMachine; namespace Compiler = CppUtils::Language::CLike; - suite.addTest("empty main", [&] { + suite.test("empty main") = [&] { constexpr auto source = uR"( keywords: [ "int" = 0 @@ -30,7 +27,7 @@ namespace CppUtils::UnitTest::Language::CLikeCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 0uz); - }); - }};*/ + expect(executionResult) == 0uz; + }; + };*/ } diff --git a/tests/Language/CSV/CSVLexer.mpp b/tests/Language/CSV/CSVLexer.mpp index 6049e2c2..baf8d609 100644 --- a/tests/Language/CSV/CSVLexer.mpp +++ b/tests/Language/CSV/CSVLexer.mpp @@ -5,82 +5,82 @@ import CppUtils; namespace CppUtils::UnitTest::Language::CSV::CSVLexer { - auto _ = TestSuite{"Language/CSV/CSVLexer", {}, [](TestSuite& suite) { + auto _ = testSuite("Language/CSV/CSVLexer") = [](TestSuite& suite) { using namespace CppUtils::String::Literals; using namespace CppUtils::Language::CSV::Literals; - suite.addTest("Empty CSV", [&] { + suite.test("Empty CSV") = [&] { const auto tree = ""_csv_tree; - suite.expect(std::ranges::empty(tree.nodes)); - }); + expect(std::ranges::empty(tree.nodes)); + }; - suite.addTest("Simple rows and columns", [&] { + suite.test("Simple rows and columns") = [&] { const auto tree = R"(a;b c;d)"_csv_tree; - suite.expectEqual(std::ranges::size(tree.nodes), 2uz); + expect(std::ranges::size(tree.nodes)) == 2uz; const auto& row0 = tree.nodes.at(0); const auto& row1 = tree.nodes.at(1); - suite.expectEqual(std::get(row0.value), "record"_token); - suite.expectEqual(std::get(row1.value), "record"_token); + expect(std::get(row0.value)) == "record"_token; + expect(std::get(row1.value)) == "record"_token; - suite.expectEqual(std::ranges::size(row0.nodes), 2uz); - suite.expectEqual(std::ranges::size(row1.nodes), 2uz); + expect(std::ranges::size(row0.nodes)) == 2uz; + expect(std::ranges::size(row1.nodes)) == 2uz; - suite.expectEqual(std::get(row0.nodes.at(0).nodes.at(0).value), "a"); - suite.expectEqual(std::get(row0.nodes.at(1).nodes.at(0).value), "b"); - suite.expectEqual(std::get(row1.nodes.at(0).nodes.at(0).value), "c"); - suite.expectEqual(std::get(row1.nodes.at(1).nodes.at(0).value), "d"); - }); + expect(std::get(row0.nodes.at(0).nodes.at(0).value)) == "a"; + expect(std::get(row0.nodes.at(1).nodes.at(0).value)) == "b"; + expect(std::get(row1.nodes.at(0).nodes.at(0).value)) == "c"; + expect(std::get(row1.nodes.at(1).nodes.at(0).value)) == "d"; + }; - suite.addTest("Quoted field with delimiter", [&] { + suite.test("Quoted field with delimiter") = [&] { const auto tree = R"("a;b";c)"_csv_tree; - suite.expectEqual(std::ranges::size(tree.nodes), 1uz); + expect(std::ranges::size(tree.nodes)) == 1uz; const auto& row = tree.nodes.front(); - suite.expectEqual(std::ranges::size(row.nodes), 2uz); - suite.expectEqual(std::get(row.nodes.at(0).nodes.at(0).value), "a;b"); - suite.expectEqual(std::get(row.nodes.at(1).nodes.at(0).value), "c"); - }); + expect(std::ranges::size(row.nodes)) == 2uz; + expect(std::get(row.nodes.at(0).nodes.at(0).value)) == "a;b"; + expect(std::get(row.nodes.at(1).nodes.at(0).value)) == "c"; + }; - suite.addTest("Quoted field with escaped quote", [&] { + suite.test("Quoted field with escaped quote") = [&] { const auto tree = R"("foo""bar";baz)"_csv_tree; - suite.expectEqual(std::ranges::size(tree.nodes), 1uz); + expect(std::ranges::size(tree.nodes)) == 1uz; const auto& row = tree.nodes.front(); - suite.expectEqual(std::ranges::size(row.nodes), 2uz); - suite.expectEqual(std::get(row.nodes.at(0).nodes.at(0).value), R"(foo"bar)"); - suite.expectEqual(std::get(row.nodes.at(1).nodes.at(0).value), "baz"); - }); + expect(std::ranges::size(row.nodes)) == 2uz; + expect(std::get(row.nodes.at(0).nodes.at(0).value)) == R"(foo"bar)"; + expect(std::get(row.nodes.at(1).nodes.at(0).value)) == "baz"; + }; - suite.addTest("Quoted field with multiline newline", [&] { + suite.test("Quoted field with multiline newline") = [&] { const auto tree = "\"line1\nline2\";value"_csv_tree; - suite.expectEqual(std::ranges::size(tree.nodes), 1uz); + expect(std::ranges::size(tree.nodes)) == 1uz; const auto& row = tree.nodes.front(); - suite.expectEqual(std::ranges::size(row.nodes), 2uz); - suite.expectEqual(std::get(row.nodes.at(0).nodes.at(0).value), "line1\nline2"); - suite.expectEqual(std::get(row.nodes.at(1).nodes.at(0).value), "value"); - }); + expect(std::ranges::size(row.nodes)) == 2uz; + expect(std::get(row.nodes.at(0).nodes.at(0).value)) == "line1\nline2"; + expect(std::get(row.nodes.at(1).nodes.at(0).value)) == "value"; + }; - suite.addTest("toTable method", [&] { + suite.test("toTable method") = [&] { const auto lexer = CppUtils::Language::CSV::CSVLexer{';'}; const auto table = lexer.toTable(R"(header1;header2 value1;value2)"); - suite.expectEqual(std::ranges::size(table), 2uz); - suite.expectEqual(table[0], std::vector{"header1", "header2"}); - suite.expectEqual(table[1], std::vector{"value1", "value2"}); - }); + expect(std::ranges::size(table)) == 2uz; + expect(table[0]) == std::vector{"header1", "header2"}; + expect(table[1]) == std::vector{"value1", "value2"}; + }; - suite.addTest("Custom delimiter comma", [&] { + suite.test("Custom delimiter comma") = [&] { const auto table = CppUtils::Language::CSV::parseTable(R"(name,age,"city, country" Alice,30,"Paris, France")", ','); - suite.expectEqual(std::ranges::size(table), 2uz); - suite.expectEqual(table[0], std::vector{"name", "age", "city, country"}); - suite.expectEqual(table[1], std::vector{"Alice", "30", "Paris, France"}); - }); - }}; + expect(std::ranges::size(table)) == 2uz; + expect(table[0]) == std::vector{"name", "age", "city, country"}; + expect(table[1]) == std::vector{"Alice", "30", "Paris, France"}; + }; + }; } diff --git a/tests/Language/CSV/Mapping.mpp b/tests/Language/CSV/Mapping.mpp index 2625b6b3..836752fe 100644 --- a/tests/Language/CSV/Mapping.mpp +++ b/tests/Language/CSV/Mapping.mpp @@ -111,51 +111,51 @@ namespace CppUtils::UnitTest::Language::CSV::Mapping } }; - auto _ = TestSuite{"Language/CSV/Mapping", {"FileSystem/Directory", "FileSystem/File", "Language/CSV/Parsing"}, [](TestSuite& suite) { + auto _ = testSuite("Language/CSV/Mapping").dependsOn("FileSystem/Directory", "FileSystem/File", "Language/CSV/Parsing") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::Language::CSV::Literals; - suite.addTest("CSV line to Struct", [&] { + suite.test("CSV line to Struct") = [&] { const auto csvLine = "foo;bar;42;-1;3.14"_csv[0]; const auto object = CppUtils::Language::CSV::toStruct(csvLine); - suite.expectEqual(object, Object{"foo", "bar", 42, -1, 3.14f}); - }); + expect(object) == Object{"foo", "bar", 42, -1, 3.14f}; + }; - suite.addTest("CSV to Structs", [&] { + suite.test("CSV to Structs") = [&] { const auto objects = CppUtils::Language::CSV::toStructs(R"( a;b;c;d;e foo;bar;42;-1;3.14 )"_csv, 1uz); - suite.expectEqual(objects, std::vector{{"foo", "bar", 42, -1, 3.14f}}); - }); + expect(objects) == std::vector{{"foo", "bar", 42, -1, 3.14f}}; + }; - suite.addTest("Struct to CSV line", [&] { + suite.test("Struct to CSV line") = [&] { const auto object = Object{"foo", "bar", 42, -1, 3.14f}; const auto csvLine = CppUtils::Language::CSV::toCSV(object); - suite.expectEqual(csvLine, "foo;bar;42;-1;3.14"s); - }); + expect(csvLine) == "foo;bar;42;-1;3.14"; + }; - suite.addTest("Structs to CSV", [&] { + suite.test("Structs to CSV") = [&] { const auto objects = std::vector{ {"foo", "bar", 42, -1, 3.14f}, {"hello", "world", 123, -456, 1.23f}}; const auto csv = CppUtils::Language::CSV::toCSV(objects); - suite.expectEqual(CppUtils::Language::CSV::parseCSV(csv), R"( + expect(CppUtils::Language::CSV::parseCSV(csv)) == R"( foo;bar;42;-1;3.14 hello;world;123;-456;1.23 - )"_csv); - }); + )"_csv; + }; - suite.addTest("load CSV File", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("load CSV File") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto filePath = directory / "test.csv"; CppUtils::FileSystem::String::write(filePath, R"( a;b;c;d;e @@ -164,11 +164,11 @@ namespace CppUtils::UnitTest::Language::CSV::Mapping const auto objects = CppUtils::Language::CSV::loadFile(filePath, 1uz); - suite.expectEqual(objects, std::vector{{"foo", "bar", 42, -1, 3.14f}}); + expect(objects) == std::vector{{"foo", "bar", 42, -1, 3.14f}}; }}; - }); + }; - suite.addTest("LambdaMapping", [&] { + suite.test("LambdaMapping") = [&] { struct LambdaMapping final { using CSVMapping = UnitTest::Language::CSV::Mapping::CSVMapping; @@ -188,10 +188,10 @@ namespace CppUtils::UnitTest::Language::CSV::Mapping const auto object = CppUtils::Language::CSV::toStruct(csvLine); - suite.expectEqual(object, Object{"foo", "bar", 42, -1, 3.14f}); - }); + expect(object) == Object{"foo", "bar", 42, -1, 3.14f}; + }; - suite.addTest("FunctorMapping", [&] { + suite.test("FunctorMapping") = [&] { struct FunctorMapping final { using CSVMapping = UnitTest::Language::CSV::Mapping::CSVMapping; @@ -204,36 +204,36 @@ namespace CppUtils::UnitTest::Language::CSV::Mapping const auto object = CppUtils::Language::CSV::toStruct(csvLine); - suite.expectEqual(object, Object{"foo", "bar", 42, -1, 3.14f}); - }); + expect(object) == Object{"foo", "bar", 42, -1, 3.14f}; + }; - suite.addTest("toStructSafe valid", [&] { + suite.test("toStructSafe valid") = [&] { const auto csvLine = "foo;bar;42;-1;3.14"_csv[0]; const auto result = CppUtils::Language::CSV::toStructSafe(csvLine); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), Object{"foo", "bar", 42, -1, 3.14f}); - }); + expect(result.has_value()); + expect(result.value()) == Object{"foo", "bar", 42, -1, 3.14f}; + }; - suite.addTest("toStructSafe invalid numeric", [&] { + suite.test("toStructSafe invalid numeric") = [&] { const auto csvLine = "foo;bar;NaN;-1;3.14"_csv[0]; const auto result = CppUtils::Language::CSV::toStructSafe(csvLine); - suite.expect(not result.has_value()); - suite.expectEqual(result.error().rawLine, "foo;bar;NaN;-1;3.14"sv); - suite.expect(not std::ranges::empty(result.error().reason)); - }); + expect(not result.has_value()); + expect(result.error().rawLine) == "foo;bar;NaN;-1;3.14"; + expect(not std::ranges::empty(result.error().reason)); + }; - suite.addTest("toStructSafe missing column", [&] { + suite.test("toStructSafe missing column") = [&] { const auto csvLine = "foo;bar"_csv[0]; const auto result = CppUtils::Language::CSV::toStructSafe(csvLine); - suite.expect(not result.has_value()); - suite.expectEqual(result.error().rawLine, "foo;bar"sv); - suite.expect(not std::ranges::empty(result.error().reason)); - }); + expect(not result.has_value()); + expect(result.error().rawLine) == "foo;bar"; + expect(not std::ranges::empty(result.error().reason)); + }; - suite.addTest("toStructSafe with Ranges::routeErrors", [&] { + suite.test("toStructSafe with Ranges::routeErrors") = [&] { const auto inputCSVLines = R"( foo;bar;42;-1;3.14 bad;line;not_num;0;0.0 @@ -249,17 +249,17 @@ namespace CppUtils::UnitTest::Language::CSV::Mapping }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputObjects), 2uz); - suite.expectEqual(outputObjects[0], Object{"foo", "bar", 42, -1, 3.14f}); - suite.expectEqual(outputObjects[1], Object{"hello", "world", 100, -200, 1.5f}); + expect(std::ranges::size(outputObjects)) == 2uz; + expect(outputObjects[0]) == Object{"foo", "bar", 42, -1, 3.14f}; + expect(outputObjects[1]) == Object{"hello", "world", 100, -200, 1.5f}; - suite.expectEqual(std::ranges::size(capturedErrors), 1uz); - suite.expect(capturedErrors[0].rawLine.contains("bad;line;not_num;0;0.0")); - suite.expect(not std::ranges::empty(capturedErrors[0].reason)); - }); + expect(std::ranges::size(capturedErrors)) == 1uz; + expect(capturedErrors[0].rawLine.contains("bad;line;not_num;0;0.0")); + expect(not std::ranges::empty(capturedErrors[0].reason)); + }; - suite.addTest("routeRejected", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("routeRejected") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto inputFilePath = directory / "input.csv"; const auto outputFilePath = directory / "output.csv"; const auto rejectedFilePath = directory / "rejected.csv"; @@ -283,9 +283,9 @@ missing_cols)"); | std::views::filter([](auto line) { return not std::ranges::empty(line); }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(cleanLines), 2uz); - suite.expectEqual(cleanLines[0], "foo;bar;42;-1;3.14"); - suite.expectEqual(cleanLines[1], "hello;world;100;-200;1.5"); + expect(std::ranges::size(cleanLines)) == 2uz; + expect(cleanLines[0]) == "foo;bar;42;-1;3.14"; + expect(cleanLines[1]) == "hello;world;100;-200;1.5"; const auto rejectedContent = CppUtils::FileSystem::String::read(rejectedFilePath); auto rejectedLines = rejectedContent @@ -294,14 +294,14 @@ missing_cols)"); | std::views::filter([](auto line) { return not std::ranges::empty(line); }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(rejectedLines), 2uz); - suite.expect(rejectedLines[0].starts_with("bad;line;not_num;0;0.0;ERROR:")); - suite.expect(rejectedLines[1].starts_with("missing_cols;ERROR:")); + expect(std::ranges::size(rejectedLines)) == 2uz; + expect(rejectedLines[0].starts_with("bad;line;not_num;0;0.0;ERROR:")); + expect(rejectedLines[1].starts_with("missing_cols;ERROR:")); }}; - }); + }; - suite.addTest("recover", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("recover") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto inputFilePath = directory / "input.csv"; const auto outputFilePath = directory / "output.csv"; @@ -325,11 +325,11 @@ hello;world;100;-200;1.5)"); | std::views::filter([](auto line) { return not std::ranges::empty(line); }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(cleanLines), 3uz); - suite.expectEqual(cleanLines[0], "foo;bar;42;-1;3.14"); - suite.expectEqual(cleanLines[1], "repaired;corrupted_line;0;0;0"); - suite.expectEqual(cleanLines[2], "hello;world;100;-200;1.5"); + expect(std::ranges::size(cleanLines)) == 3uz; + expect(cleanLines[0]) == "foo;bar;42;-1;3.14"; + expect(cleanLines[1]) == "repaired;corrupted_line;0;0;0"; + expect(cleanLines[2]) == "hello;world;100;-200;1.5"; }}; - }); - }}; + }; + }; } diff --git a/tests/Language/CSV/Parsing.mpp b/tests/Language/CSV/Parsing.mpp index 7a23455a..47e35271 100644 --- a/tests/Language/CSV/Parsing.mpp +++ b/tests/Language/CSV/Parsing.mpp @@ -4,35 +4,35 @@ import CppUtils; namespace CppUtils::UnitTest::Language::CSV::Parsing { - auto _ = TestSuite{"Language/CSV/Parsing", {}, [](TestSuite& suite) { + auto _ = testSuite("Language/CSV/Parsing") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::Language::CSV::Literals; - suite.addTest("_csv literal", [&] { + suite.test("_csv literal") = [&] { const auto csv = "a;b\nc;d"_csv; - suite.expectEqual(std::ranges::size(csv), 2uz); - suite.expectEqual(csv[0], std::vector{"a", "b"}); - suite.expectEqual(csv[1], std::vector{"c", "d"}); - }); + expect(std::ranges::size(csv)) == 2uz; + expect(csv[0]) == std::vector{"a", "b"}; + expect(csv[1]) == std::vector{"c", "d"}; + }; - suite.addTest("parseCSV with multiple lines and spaces", [&] { + suite.test("parseCSV with multiple lines and spaces") = [&] { const auto csv = CppUtils::Language::CSV::parseCSV(R"( header1 ; header2 value1 ; value2 )"); - suite.expectEqual(std::ranges::size(csv), 2uz); - suite.expectEqual(csv[0], std::vector{"header1", "header2"}); - suite.expectEqual(csv[1], std::vector{"value1", "value2"}); - }); + expect(std::ranges::size(csv)) == 2uz; + expect(csv[0]) == std::vector{"header1", "header2"}; + expect(csv[1]) == std::vector{"value1", "value2"}; + }; - suite.addTest("splitLine", [&] { + suite.test("splitLine") = [&] { const auto values = CppUtils::Language::CSV::splitLine(" foo ; bar "); - suite.expectEqual(std::ranges::size(values), 2uz); - suite.expectEqual(values[0], "foo"sv); - suite.expectEqual(values[1], "bar"sv); - }); - }}; + expect(std::ranges::size(values)) == 2uz; + expect(values[0]) == "foo"; + expect(values[1]) == "bar"; + }; + }; } diff --git a/tests/Language/CSV/Range.mpp b/tests/Language/CSV/Range.mpp index bece105e..1135a400 100644 --- a/tests/Language/CSV/Range.mpp +++ b/tests/Language/CSV/Range.mpp @@ -30,8 +30,8 @@ namespace CppUtils::UnitTest::Language::CSV::Range using FunctionMapping = CppUtils::Type::Mapping<>; }; - auto _ = TestSuite{"Language/CSV/Range", {"Language/CSV/Parsing", "Language/CSV/Mapping", "FileSystem/File", "FileSystem/Directory"}, [](TestSuite& suite) { - suite.addTest("Full Pipeline", [&] { + auto _ = testSuite("Language/CSV/Range").dependsOn("Language/CSV/Parsing", "Language/CSV/Mapping", "FileSystem/File", "FileSystem/Directory") = [](TestSuite& suite) { + suite.test("Full Pipeline") = [&] { CppUtils::FileSystem::TemporaryDirectory{[&](const auto& directory) { const auto inputFilePath = directory / "input.csv"; const auto outputFilePath = directory / "output.csv"; @@ -59,11 +59,11 @@ Charlie;40)"); | std::views::filter([](auto line) { return not std::ranges::empty(line); }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(lines), 3uz); - suite.expectEqual(lines[0], "Alice;31"); - suite.expectEqual(lines[1], "Bob;26"); - suite.expectEqual(lines[2], "Charlie;41"); + expect(std::ranges::size(lines)) == 3uz; + expect(lines[0]) == "Alice;31"; + expect(lines[1]) == "Bob;26"; + expect(lines[2]) == "Charlie;41"; }}; - }); - }}; + }; + }; } diff --git a/tests/Language/CallStackCompiler.mpp b/tests/Language/CallStackCompiler.mpp index 8023da78..feb84e6b 100644 --- a/tests/Language/CallStackCompiler.mpp +++ b/tests/Language/CallStackCompiler.mpp @@ -6,25 +6,22 @@ import CppUtils; namespace CppUtils::UnitTest::Language::CallStackCompiler { /* - auto _ = TestSuite{ - "Language/CallStackCompiler", - {"Logger", "Language/LabelsCompiler"}, - [](auto& suite) { + auto _ = testSuite("Language/CallStackCompiler").dependsOn("Logger", "Language/LabelsCompiler") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; namespace VM = CppUtils::Language::VirtualMachine; namespace Compiler = CppUtils::Language::CallStackCompiler; - suite.addTest("empty source", [&] { + suite.test("empty source") = [&] { constexpr auto source = u"§{X}"sv; auto output = Compiler::compile(source); Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 0uz); - }); + expect(executionResult) == 0uz; + }; - suite.addTest("one function", [&] { + suite.test("one function") = [&] { constexpr auto source = uR"( (21X @@ -38,10 +35,10 @@ namespace CppUtils::UnitTest::Language::CallStackCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42uz); - }); + expect(executionResult) == 42uz; + }; - suite.addTest("comment", [&] { + suite.test("comment") = [&] { constexpr auto source = uR"( main §{ 4 @@ -53,10 +50,10 @@ namespace CppUtils::UnitTest::Language::CallStackCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42uz); - }); + expect(executionResult) == 42uz; + }; - suite.addTest("call function", [&] { // constexpr { (not supported on MSVC) + suite.test("call function") = [&] { // constexpr { (not supported on MSVC) constexpr auto source = uR"( (21X @@ -81,8 +78,8 @@ namespace CppUtils::UnitTest::Language::CallStackCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42uz); - }); - }}; + expect(executionResult) == 42uz; + }; + }; */ } diff --git a/tests/Language/GrammarParser.mpp b/tests/Language/GrammarParser.mpp index 9a9a36e1..abd8b663 100644 --- a/tests/Language/GrammarParser.mpp +++ b/tests/Language/GrammarParser.mpp @@ -5,15 +5,12 @@ import CppUtils; namespace CppUtils::UnitTest::Language::GrammarParser { - auto _ = TestSuite{ - "Language/GrammarParser", - {"Language/ASTParser"}, - [](TestSuite& suite) { + auto _ = testSuite("Language/GrammarParser").dependsOn("Language/ASTParser") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::String::Literals; using Logger = TestSuite::Logger; - suite.addTest("Empty source", [&] { + suite.test("Empty source") = [&] { auto interpreter = CppUtils::Language::GrammarParser{}; constexpr auto source = ""sv; auto cursor = CppUtils::String::Cursor{source}; @@ -22,7 +19,7 @@ namespace CppUtils::UnitTest::Language::GrammarParser if (not result) Logger::print<"error">("Error: {}\nPosition: {}\nChar: '{}'", result.error(), cursor.position, cursor.getCurrent()); - suite.expect(result); - }); - }}; + expect(result); + }; + }; } diff --git a/tests/Language/INI/IniLexer.mpp b/tests/Language/INI/IniLexer.mpp index fccdfa1a..ea88eff8 100644 --- a/tests/Language/INI/IniLexer.mpp +++ b/tests/Language/INI/IniLexer.mpp @@ -7,12 +7,12 @@ namespace CppUtils::UnitTest::Language::Ini::IniLexer { using namespace CppUtils::String::Literals; - auto _ = TestSuite{"Language/Ini/IniLexer", {"Language/JSON/JsonLexer"}, [](TestSuite& suite) { + auto _ = testSuite("Language/Ini/IniLexer").dependsOn("Language/JSON/JsonLexer") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::Language::Ini::Literals; using namespace CppUtils::Language::JSON::Literals; - suite.addTest("Minimalist", [&] { + suite.test("Minimalist") = [&] { const auto iniTree = R"( [Section 1] key=value @@ -29,10 +29,10 @@ namespace CppUtils::UnitTest::Language::Ini::IniLexer } )"_json_tree; - suite.expectEqual(iniTree, jsonTree); - }); + expect(iniTree) == jsonTree; + }; - suite.addTest("Number", [&] { + suite.test("Number") = [&] { const auto iniTree = R"( [Numbers] pi=3.14 @@ -49,10 +49,10 @@ namespace CppUtils::UnitTest::Language::Ini::IniLexer } )"_json_tree; - suite.expectEqual(iniTree, jsonTree); - }); + expect(iniTree) == jsonTree; + }; - suite.addTest("Comments", [&] { + suite.test("Comments") = [&] { const auto iniTree = R"( ;comment [Section 1];comment @@ -71,10 +71,10 @@ namespace CppUtils::UnitTest::Language::Ini::IniLexer } )"_json_tree; - suite.expectEqual(iniTree, jsonTree); - }); + expect(iniTree) == jsonTree; + }; - suite.addTest("Full", [&] { + suite.test("Full") = [&] { const auto iniTree = R"( [Section 1] ; comment @@ -110,10 +110,10 @@ namespace CppUtils::UnitTest::Language::Ini::IniLexer } )"_json_tree; - suite.expectEqual(std::ranges::size(iniTree.nodes), 3uz); - suite.expectEqual(iniTree.nodes.at(0), jsonTree.nodes.at(0)); - suite.expectEqual(std::ranges::size(iniTree.nodes.at(1)["Numbers"_token].nodes), 8uz); - suite.expectEqual(std::ranges::size(iniTree.nodes.at(2)["Booleans"_token].nodes), 2uz); - }); - }}; + expect(std::ranges::size(iniTree.nodes)) == 3uz; + expect(iniTree.nodes.at(0)) == jsonTree.nodes.at(0); + expect(std::ranges::size(iniTree.nodes.at(1)["Numbers"_token].nodes)) == 8uz; + expect(std::ranges::size(iniTree.nodes.at(2)["Booleans"_token].nodes)) == 2uz; + }; + }; } diff --git a/tests/Language/JSON/JsonLexer.mpp b/tests/Language/JSON/JsonLexer.mpp index c508f622..ecdadbb7 100644 --- a/tests/Language/JSON/JsonLexer.mpp +++ b/tests/Language/JSON/JsonLexer.mpp @@ -7,20 +7,20 @@ namespace CppUtils::UnitTest::Language::JSON::JsonLexer { using namespace CppUtils::String::Literals; - auto _ = TestSuite{"Language/JSON/JsonLexer", {}, [](TestSuite& suite) { + auto _ = testSuite("Language/JSON/JsonLexer") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::Language::JSON::Literals; - suite.addTest("Empty json", [&] { + suite.test("Empty json") = [&] { const auto jsonTree = R"( {} )"_json_tree; CppUtils::Log::TreeNodeLogger::log(jsonTree); - suite.expect(std::ranges::empty(jsonTree.nodes)); - }); + expect(std::ranges::empty(jsonTree.nodes)); + }; - suite.addTest("Null", [&] { + suite.test("Null") = [&] { const auto jsonTree = R"( { "key": null @@ -28,13 +28,13 @@ namespace CppUtils::UnitTest::Language::JSON::JsonLexer )"_json_tree; CppUtils::Log::TreeNodeLogger::log(jsonTree); - suite.expectEqual(std::ranges::size(jsonTree.nodes), 1uz); - suite.expect(jsonTree.exists("key"_token)); + expect(std::ranges::size(jsonTree.nodes)) == 1uz; + expect(jsonTree.exists("key"_token)); const auto& keyNode = jsonTree["key"_token]; - suite.expect(keyNode.exists("null"_token)); - }); + expect(keyNode.exists("null"_token)); + }; - suite.addTest("Boolean", [&] { + suite.test("Boolean") = [&] { const auto jsonTree = R"( { "bool0": true, @@ -43,18 +43,18 @@ namespace CppUtils::UnitTest::Language::JSON::JsonLexer )"_json_tree; CppUtils::Log::TreeNodeLogger::log(jsonTree); - suite.expectEqual(std::ranges::size(jsonTree.nodes), 2uz); - suite.expect(jsonTree.exists("bool0"_token)); - suite.expect(jsonTree.exists("bool1"_token)); + expect(std::ranges::size(jsonTree.nodes)) == 2uz; + expect(jsonTree.exists("bool0"_token)); + expect(jsonTree.exists("bool1"_token)); const auto& bool0Node = jsonTree["bool0"_token]; const auto& bool1Node = jsonTree["bool1"_token]; - suite.expect(bool0Node.exists("boolean"_token)); - suite.expect(bool1Node.exists("boolean"_token)); - suite.expectEqual(std::get(bool0Node["boolean"_token].nodes.at(0).value), true); - suite.expectEqual(std::get(bool1Node["boolean"_token].nodes.at(0).value), false); - }); + expect(bool0Node.exists("boolean"_token)); + expect(bool1Node.exists("boolean"_token)); + expect(std::get(bool0Node["boolean"_token].nodes.at(0).value)); + expect(std::get(bool1Node["boolean"_token].nodes.at(0).value)) == false; + }; - suite.addTest("Number", [&] { + suite.test("Number") = [&] { const auto jsonTree = R"( { "number0": 42, @@ -64,22 +64,22 @@ namespace CppUtils::UnitTest::Language::JSON::JsonLexer )"_json_tree; CppUtils::Log::TreeNodeLogger::log(jsonTree); - suite.expectEqual(std::ranges::size(jsonTree.nodes), 3uz); - suite.expect(jsonTree.exists("number0"_token)); - suite.expect(jsonTree.exists("number1"_token)); - suite.expect(jsonTree.exists("number2"_token)); + expect(std::ranges::size(jsonTree.nodes)) == 3uz; + expect(jsonTree.exists("number0"_token)); + expect(jsonTree.exists("number1"_token)); + expect(jsonTree.exists("number2"_token)); const auto& n0 = jsonTree["number0"_token]; const auto& n1 = jsonTree["number1"_token]; const auto& n2 = jsonTree["number2"_token]; - suite.expect(n0.exists("float"_token)); - suite.expect(n1.exists("float"_token)); - suite.expect(n2.exists("float"_token)); - suite.expectEqual(std::get(n0["float"_token].nodes.at(0).value), 42.0f); - suite.expectEqual(std::get(n1["float"_token].nodes.at(0).value), -21.0f); - suite.expectEqual(std::get(n2["float"_token].nodes.at(0).value), 3.141592f); - }); - - suite.addTest("String", [&] { + expect(n0.exists("float"_token)); + expect(n1.exists("float"_token)); + expect(n2.exists("float"_token)); + expect(std::get(n0["float"_token].nodes.at(0).value)) == 42.0f; + expect(std::get(n1["float"_token].nodes.at(0).value)) == -21.0f; + expect(std::get(n2["float"_token].nodes.at(0).value)) == 3.141592f; + }; + + suite.test("String") = [&] { const auto jsonTree = R"( { "key": "hello" @@ -87,14 +87,14 @@ namespace CppUtils::UnitTest::Language::JSON::JsonLexer )"_json_tree; CppUtils::Log::TreeNodeLogger::log(jsonTree); - suite.expectEqual(std::ranges::size(jsonTree.nodes), 1uz); - suite.expect(jsonTree.exists("key"_token)); + expect(std::ranges::size(jsonTree.nodes)) == 1uz; + expect(jsonTree.exists("key"_token)); const auto& keyNode = jsonTree["key"_token]; - suite.expect(keyNode.exists("string"_token)); - suite.expectEqual(std::get(keyNode["string"_token].nodes.at(0).value), "hello"_token); - }); + expect(keyNode.exists("string"_token)); + expect(std::get(keyNode["string"_token].nodes.at(0).value)) == "hello"_token; + }; - suite.addTest("Array", [&] { + suite.test("Array") = [&] { const auto jsonTree = R"( { "key": [1, "abc", true] @@ -102,21 +102,21 @@ namespace CppUtils::UnitTest::Language::JSON::JsonLexer )"_json_tree; CppUtils::Log::TreeNodeLogger::log(jsonTree); - suite.expectEqual(std::ranges::size(jsonTree.nodes), 1uz); - suite.expect(jsonTree.exists("key"_token)); + expect(std::ranges::size(jsonTree.nodes)) == 1uz; + expect(jsonTree.exists("key"_token)); const auto& keyNode = jsonTree["key"_token]; - suite.expect(keyNode.exists("array"_token)); + expect(keyNode.exists("array"_token)); const auto& arrayNode = keyNode["array"_token]; - suite.expectEqual(std::ranges::size(arrayNode.nodes), 3uz); - suite.expectEqual(std::get(arrayNode.nodes.at(0).value), "float"_token); - suite.expectEqual(std::get(arrayNode.nodes.at(0).nodes.at(0).value), 1.0f); - suite.expectEqual(std::get(arrayNode.nodes.at(1).value), "string"_token); - suite.expectEqual(std::get(arrayNode.nodes.at(1).nodes.at(0).value), "abc"_token); - suite.expectEqual(std::get(arrayNode.nodes.at(2).value), "boolean"_token); - suite.expectEqual(std::get(arrayNode.nodes.at(2).nodes.at(0).value), true); - }); - - suite.addTest("Object", [&] { + expect(std::ranges::size(arrayNode.nodes)) == 3uz; + expect(std::get(arrayNode.nodes.at(0).value)) == "float"_token; + expect(std::get(arrayNode.nodes.at(0).nodes.at(0).value)) == 1.0f; + expect(std::get(arrayNode.nodes.at(1).value)) == "string"_token; + expect(std::get(arrayNode.nodes.at(1).nodes.at(0).value)) == "abc"_token; + expect(std::get(arrayNode.nodes.at(2).value)) == "boolean"_token; + expect(std::get(arrayNode.nodes.at(2).nodes.at(0).value)); + }; + + suite.test("Object") = [&] { const auto jsonTree = R"( { "key": { @@ -127,11 +127,11 @@ namespace CppUtils::UnitTest::Language::JSON::JsonLexer )"_json_tree; CppUtils::Log::TreeNodeLogger::log(jsonTree); - suite.expectEqual(std::ranges::size(jsonTree.nodes), 1uz); - suite.expect(jsonTree.exists("key"_token)); + expect(std::ranges::size(jsonTree.nodes)) == 1uz; + expect(jsonTree.exists("key"_token)); const auto& keyNode = jsonTree["key"_token]; - suite.expect(keyNode.exists("number0"_token)); - suite.expect(keyNode.exists("number1"_token)); - }); - }}; + expect(keyNode.exists("number0"_token)); + expect(keyNode.exists("number1"_token)); + }; + }; } diff --git a/tests/Language/JSON/Mapping.mpp b/tests/Language/JSON/Mapping.mpp index 4a4a0009..513671a7 100644 --- a/tests/Language/JSON/Mapping.mpp +++ b/tests/Language/JSON/Mapping.mpp @@ -48,11 +48,11 @@ namespace CppUtils::UnitTest::Language::JSON::Mapping using FunctionMapping = Type::Mapping<>; }; - auto _ = TestSuite{"Language/JSON/Mapping", {"Logger", "Language/JSON/JsonLexer", "Language/JSON/Parsing"}, [](TestSuite& suite) { + auto _ = testSuite("Language/JSON/Mapping").dependsOn("Logger", "Language/JSON/JsonLexer", "Language/JSON/Parsing") = [](TestSuite& suite) { using namespace CppUtils::Language::JSON::Literals; using Logger = TestSuite::Logger; - suite.addTest("JSON to Struct", [&] { + suite.test("JSON to Struct") = [&] { const auto result = R"({ "a": "test", "b": 123, @@ -60,29 +60,29 @@ namespace CppUtils::UnitTest::Language::JSON::Mapping "d": false })"_json; - suite.expect(result); + expect(result); const auto object = CppUtils::Language::JSON::toStruct(result.value()); - suite.expectEqual(object.a, "test"); - suite.expectEqual(object.b, 123); - suite.expectEqual(object.c, 45.6); - suite.expectEqual(object.d, false); - }); + expect(object.a) == "test"; + expect(object.b) == 123; + expect(object.c) == 45.6; + expect(object.d) == false; + }; - suite.addTest("JSON Array to Structs", [&] { + suite.test("JSON Array to Structs") = [&] { const auto result = R"([ {"a": "o1", "b": 1}, {"a": "o2", "b": 2} ])"_json; - suite.expect(result); + expect(result); const auto& value = result.value(); const auto objects = CppUtils::Language::JSON::toStructs(value); - suite.expectEqual(std::ranges::size(objects), 2uz); - suite.expectEqual(objects[0].a, "o1"); - suite.expectEqual(objects[1].a, "o2"); - }); + expect(std::ranges::size(objects)) == 2uz; + expect(objects[0].a) == "o1"; + expect(objects[1].a) == "o2"; + }; - suite.addTest("Struct to JSON", [&] { + suite.test("Struct to JSON") = [&] { const auto object = Object{"hello", 42, 3.14, true}; const auto json = CppUtils::Language::JSON::toJSON(object); @@ -95,11 +95,11 @@ namespace CppUtils::UnitTest::Language::JSON::Mapping "d": true })"_json; - suite.expect(result); - suite.expectEqual(json, result.value()); - }); + expect(result); + expect(json) == result.value(); + }; - suite.addTest("Structs to JSON", [&] { + suite.test("Structs to JSON") = [&] { const auto objects = std::vector{ {"o1", 1, 1.1, true}, {"o2", 2, 2.2, false}}; @@ -108,8 +108,8 @@ namespace CppUtils::UnitTest::Language::JSON::Mapping for (const auto& objectValue : objects) resultValue.as().push_back(CppUtils::Language::JSON::toJSON(objectValue)); - suite.expect(resultValue.isArray()); - suite.expectEqual(std::ranges::size(resultValue.as()), 2uz); + expect(resultValue.isArray()); + expect(std::ranges::size(resultValue.as())) == 2uz; Logger::print("Produced JSON Array: {}", static_cast(resultValue)); @@ -118,8 +118,8 @@ namespace CppUtils::UnitTest::Language::JSON::Mapping {"a": "o2", "b": 2, "c": 2.2, "d": false} ])"_json; - suite.expect(result); - suite.expectEqual(resultValue, result.value()); - }); - }}; + expect(result); + expect(resultValue) == result.value(); + }; + }; } diff --git a/tests/Language/JSON/Parsing.mpp b/tests/Language/JSON/Parsing.mpp index 1ed2a5bb..0259d333 100644 --- a/tests/Language/JSON/Parsing.mpp +++ b/tests/Language/JSON/Parsing.mpp @@ -5,18 +5,18 @@ import CppUtils; namespace CppUtils::UnitTest::Language::JSON::Parsing { - auto _ = TestSuite{"Language/JSON/Parsing", {"Language/JSON/JsonLexer"}, [](TestSuite& suite) { + auto _ = testSuite("Language/JSON/Parsing").dependsOn("Language/JSON/JsonLexer") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::Language::JSON::Literals; - suite.addTest("Simple Parse", [&] { + suite.test("Simple Parse") = [&] { const auto result = "{}"_json; - suite.expect(result); - suite.expect(result->isObject()); - suite.expect(std::ranges::empty(result->as())); - }); + expect(result); + expect(result->isObject()); + expect(std::ranges::empty(result->as())); + }; - suite.addTest("Parse Values", [&] { + suite.test("Parse Values") = [&] { const auto result = R"({ "a": "hello", "b": 42, @@ -25,40 +25,54 @@ namespace CppUtils::UnitTest::Language::JSON::Parsing "e": null })"_json; - suite.expect(result); + expect(result); const auto& value = result.value(); - suite.expect(value.isObject()); - suite.expectEqual(value["a"].template as(), "hello"); - suite.expectEqual(value["b"].template as(), 42.0); - suite.expectEqual(value["c"].template as(), 3.14); - suite.expectEqual(value["d"].template as(), true); - suite.expect(value["e"].isNull()); - }); + expect(value.isObject()); + expect(value["a"].template as()) == "hello"; + expect(value["b"].template as()) == 42.0; + expect(value["c"].template as()) == 3.14; + expect(value["d"].template as()); + expect(value["e"].isNull()); + }; - suite.addTest("Parse Array", [&] { + suite.test("Parse Array") = [&] { const auto result = "[1, 2, 3]"_json; - suite.expect(result); + expect(result); const auto& value = result.value(); - suite.expect(value.isArray()); - suite.expectEqual(std::ranges::size(value.as()), 3uz); - suite.expectEqual(value[0].as(), 1.0); - }); + expect(value.isArray()); + expect(std::ranges::size(value.as())) == 3uz; + expect(value[0].as()) == 1.0; + }; - suite.addTest("Error Reporting", [&] { + suite.test("Error Reporting") = [&] { const auto result = R"({"a": 1, "b"})"_json; - suite.expect(not result); + expect(not result); const auto& error = result.error(); - suite.expectEqual(error.message, "Expected ':' after key"); - suite.expectEqual(error.position, 12uz); - }); + expect(error.message) == "Expected ':' after key"; + expect(error.position) == 12uz; + }; - suite.addTest("Stringify", [&] { + suite.test("Stringify") = [&] { auto object = CppUtils::Language::JSON::Object{}; object["a"] = CppUtils::Language::JSON::Value{"hello"}; object["b"] = CppUtils::Language::JSON::Value{42.0}; const auto json = static_cast(CppUtils::Language::JSON::Value{object}); - suite.expectEqual(json, R"({"a":"hello","b":42})"); - }); - }}; + expect(json) == R"({"a":"hello","b":42})"; + }; + + suite.test("Robustness fuzzing against random input") = [] { + fuzz("Random string parsing should never crash", 1'000uz, [](auto& fuzzer) { + const auto randomInput = fuzzer.randomString(0uz, 64uz, "{}[]\":,0123456789truefalsenull \t\n\\/abczABCZ!@#$%^&*()"); + const auto result = CppUtils::Language::JSON::parseJSON(randomInput); + if (result) + { + const auto& value = result.value(); + expect(value.isObject() or value.isArray() or value.isString() or value.isNumber() or value.isBool() or value.isNull()); + } + else + expect(not std::ranges::empty(result.error().message)); + }); + }; + }; } diff --git a/tests/Language/LabelsCompiler.mpp b/tests/Language/LabelsCompiler.mpp index c8f7ab17..7207bd2a 100644 --- a/tests/Language/LabelsCompiler.mpp +++ b/tests/Language/LabelsCompiler.mpp @@ -6,25 +6,22 @@ import CppUtils; namespace CppUtils::UnitTest::Language::LabelsCompiler { /* - auto _ = TestSuite{ - "Language/LabelsCompiler", - {"Logger", "Language/VirtualMachine"}, - [](auto& suite) { + auto _ = testSuite("Language/LabelsCompiler").dependsOn("Logger", "Language/VirtualMachine") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; namespace VM = CppUtils::Language::VirtualMachine; namespace Compiler = CppUtils::Language::LabelsCompiler; - suite.addTest("empty source", [&] { + suite.test("empty source") = [&] { constexpr auto source = u"("sv; auto output = Compiler::compile(source); Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 0uz); - }); + expect(executionResult) == 0uz; + }; - suite.addTest("code without labels", [&] { + suite.test("code without labels") = [&] { constexpr auto source = uR"( (0:42 )"sv; @@ -32,10 +29,10 @@ namespace CppUtils::UnitTest::Language::LabelsCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42); - }); + expect(executionResult) == 42; + }; - suite.addTest("one label", [&] { + suite.test("one label") = [&] { constexpr auto source = uR"( (§(42X )"sv; @@ -43,10 +40,10 @@ namespace CppUtils::UnitTest::Language::LabelsCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42uz); - }); + expect(executionResult) == 42uz; + }; - suite.addTest("multiple labels", [&] { + suite.test("multiple labels") = [&] { constexpr auto source = uR"( (§ (§ @@ -56,10 +53,10 @@ namespace CppUtils::UnitTest::Language::LabelsCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42uz); - }); + expect(executionResult) == 42uz; + }; - suite.addTest("one jump", [&] { + suite.test("one jump") = [&] { constexpr auto source = uR"( (0¤ main @@ -74,10 +71,10 @@ namespace CppUtils::UnitTest::Language::LabelsCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42uz); - }); + expect(executionResult) == 42uz; + }; - suite.addTest("multiple jumps", [&] constexpr { + suite.test("multiple jumps") = [&] constexpr { constexpr auto source = uR"( (0¤ main @@ -97,10 +94,10 @@ namespace CppUtils::UnitTest::Language::LabelsCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42uz); - }); + expect(executionResult) == 42uz; + }; - suite.addTest("escape §", [&] { + suite.test("escape §") = [&] { constexpr auto source = uR"( (0¤ @@ -112,10 +109,10 @@ namespace CppUtils::UnitTest::Language::LabelsCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42uz); - }); + expect(executionResult) == 42uz; + }; - suite.addTest("escape ¤", [&] { + suite.test("escape ¤") = [&] { constexpr auto source = uR"( (0\¤ @@ -129,8 +126,8 @@ namespace CppUtils::UnitTest::Language::LabelsCompiler Logger::print("Output:\n{}\n", CppUtils::String::toAscii(output)); auto executionResult = VM::execute(output); - suite.expectEqual(executionResult, 42uz); - }); - }}; + expect(executionResult) == 42uz; + }; + }; */ } diff --git a/tests/Language/Lexer/Grammar/HighLevelGrammar.mpp b/tests/Language/Lexer/Grammar/HighLevelGrammar.mpp index 00781268..847f1e2f 100644 --- a/tests/Language/Lexer/Grammar/HighLevelGrammar.mpp +++ b/tests/Language/Lexer/Grammar/HighLevelGrammar.mpp @@ -8,10 +8,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar using namespace CppUtils::String::Literals; using namespace CppUtils::Language::Lexer::Grammar::Literals; - auto _ = TestSuite{"Language/Lexer/Grammar/highLevelGrammar", {}, [](TestSuite& suite) { + auto _ = testSuite("Language/Lexer/Grammar/highLevelGrammar") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("empty grammar", [&] { + suite.test("empty grammar") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto highLevelGrammarAst = CppUtils::Language::Lexer::parse(CppUtils::Language::Lexer::Grammar::highLevelGrammarSrc, lowLevelGrammarAst); std::println("{}", highLevelGrammarAst); @@ -23,10 +23,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(""sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("end", [&] { + suite.test("end") = [&] { const auto grammarAst = R"( main: end(); )"_grammar; @@ -34,10 +34,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(""sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("comment", [&] { + suite.test("comment") = [&] { const auto grammarAst = R"( # entry point main: ; @@ -46,10 +46,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(""sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("char", [&] { + suite.test("char") = [&] { const auto grammarAst = R"( main: 'c'; )"_grammar; @@ -57,10 +57,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("is char", [&] { + suite.test("is char") = [&] { const auto grammarAst = R"( main: is('c') 'c'; )"_grammar; @@ -68,10 +68,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("escaped chars", [&] { + suite.test("escaped chars") = [&] { const auto grammarAst = R"( main: "\ \n\t\r\""; )"_grammar; @@ -79,10 +79,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(" \n\t\r\""sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("add char", [&] { + suite.test("add char") = [&] { const auto grammarAst = R"( main: 'c' add('c'); )"_grammar; @@ -90,11 +90,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('c')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('c'); + }; - suite.addTest("read char", [&] { + suite.test("read char") = [&] { const auto grammarAst = R"( main: read()+; )"_grammar; @@ -102,11 +102,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('c')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('c'); + }; - suite.addTest("token", [&] { + suite.test("token") = [&] { const auto grammarAst = R"( main: c; c: 'c'; @@ -115,10 +115,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("string", [&] { + suite.test("string") = [&] { const auto grammarAst = R"( main: word; word: "Hello World!"; @@ -127,10 +127,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("Hello World!"sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("recurrence", [&] { + suite.test("recurrence") = [&] { const auto grammarAst = R"( main: chars; chars: char chars; chars: char; @@ -140,12 +140,12 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("AAA"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 3uz); + expect(std::ranges::size(outputAst.root.nodes)) == 3uz; for (auto i = 0uz; i < 3uz; ++i) - suite.expectEqual(outputAst.root.nodes[i].value, static_cast('A')); - }); + expect(outputAst.root.nodes[i].value) == static_cast('A'); + }; - suite.addTest("optional", [&] { + suite.test("optional") = [&] { const auto grammarAst = R"( main: chars; chars: char ~chars; @@ -155,12 +155,12 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("AAA"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 3uz); + expect(std::ranges::size(outputAst.root.nodes)) == 3uz; for (auto i = 0uz; i < 3uz; ++i) - suite.expectEqual(outputAst.root.nodes[i].value, static_cast('A')); - }); + expect(outputAst.root.nodes[i].value) == static_cast('A'); + }; - suite.addTest("or", [&] { + suite.test("or") = [&] { const auto grammarAst = R"( main: or('A', 'B') read()+; )"_grammar; @@ -168,11 +168,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("BC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('C')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('C'); + }; - suite.addTest("parenthesis", [&] { + suite.test("parenthesis") = [&] { const auto grammarAst = R"( main: ('A' 'B') read()+; )"_grammar; @@ -180,11 +180,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("ABC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('C')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('C'); + }; - suite.addTest("comparison", [&] { + suite.test("comparison") = [&] { const auto grammarAst = R"( main: char; char: or(min, maj); @@ -195,11 +195,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("C"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, "maj"_token); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == "maj"_token; + }; - suite.addTest("hash", [&] { + suite.test("hash") = [&] { const auto grammarAst = R"( main: hash(keyword); keyword: read()+ ~keyword; @@ -208,11 +208,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("variable"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, "variable"_token); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == "variable"_token; + }; - suite.addTest("not", [&] { + suite.test("not") = [&] { const auto grammarAst = R"( main: quote; quote: '"' ~quoteContent '"'; @@ -222,15 +222,15 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(R"("Hello")"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 5uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('H')); - suite.expectEqual(outputAst.root.nodes[1].value, static_cast('e')); - suite.expectEqual(outputAst.root.nodes[2].value, static_cast('l')); - suite.expectEqual(outputAst.root.nodes[3].value, static_cast('l')); - suite.expectEqual(outputAst.root.nodes[4].value, static_cast('o')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 5uz; + expect(outputAst.root.nodes[0].value) == static_cast('H'); + expect(outputAst.root.nodes[1].value) == static_cast('e'); + expect(outputAst.root.nodes[2].value) == static_cast('l'); + expect(outputAst.root.nodes[3].value) == static_cast('l'); + expect(outputAst.root.nodes[4].value) == static_cast('o'); + }; - suite.addTest("sub", [&] { + suite.test("sub") = [&] { const auto grammarAst = R"( main: node add(';'); node: read()+ ~sub(node); @@ -239,20 +239,20 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("ABC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 2uz); + expect(std::ranges::size(outputAst.root.nodes)) == 2uz; const auto& aNode = outputAst.root.nodes[0]; - suite.expectEqual(aNode.value, static_cast('A')); - suite.expectEqual(std::ranges::size(aNode.nodes), 1uz); + expect(aNode.value) == static_cast('A'); + expect(std::ranges::size(aNode.nodes)) == 1uz; const auto& bNode = aNode.nodes[0]; - suite.expectEqual(bNode.value, static_cast('B')); - suite.expectEqual(std::ranges::size(bNode.nodes), 1uz); + expect(bNode.value) == static_cast('B'); + expect(std::ranges::size(bNode.nodes)) == 1uz; const auto& cNode = bNode.nodes[0]; - suite.expectEqual(cNode.value, static_cast('C')); - suite.expect(std::ranges::empty(cNode.nodes)); - suite.expectEqual(outputAst.root.nodes[1].value, static_cast(';')); - }); + expect(cNode.value) == static_cast('C'); + expect(std::ranges::empty(cNode.nodes)); + expect(outputAst.root.nodes[1].value) == static_cast(';'); + }; - suite.addTest("repeat", [&] { + suite.test("repeat") = [&] { const auto grammarAst = R"( main: repeat('A') repeat('B', ',') read()+; )"_grammar; @@ -260,11 +260,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("AAAB,B,BC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('C')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('C'); + }; - suite.addTest("push-pop", [&] { + suite.test("push-pop") = [&] { const auto grammarAst = R"( main: push(hash(keyword)) pop(); keyword: read()+ ~keyword; @@ -273,8 +273,8 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::HighLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("ABC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, "ABC"_token); - }); - }}; + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == "ABC"_token; + }; + }; } diff --git a/tests/Language/Lexer/Grammar/LowLevelGrammar.mpp b/tests/Language/Lexer/Grammar/LowLevelGrammar.mpp index c1b8c4bf..1127a047 100644 --- a/tests/Language/Lexer/Grammar/LowLevelGrammar.mpp +++ b/tests/Language/Lexer/Grammar/LowLevelGrammar.mpp @@ -7,10 +7,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar { using namespace CppUtils::String::Literals; - auto _ = TestSuite{"Language/Lexer/Grammar/lowLevelGrammar", {}, [](TestSuite& suite) { + auto _ = testSuite("Language/Lexer/Grammar/lowLevelGrammar") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("empty grammar", [&] { + suite.test("empty grammar") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); std::println("{}", lowLevelGrammarAst); @@ -22,10 +22,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(""sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("end", [&] { + suite.test("end") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -36,10 +36,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(""sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("comment", [&] { + suite.test("comment") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -51,10 +51,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(""sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("char", [&] { + suite.test("char") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -65,10 +65,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("is char", [&] { + suite.test("is char") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -79,10 +79,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("escaped chars", [&] { + suite.test("escaped chars") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -93,10 +93,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(" \n\t\r\""sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("add char", [&] { + suite.test("add char") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -107,11 +107,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('c')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('c'); + }; - suite.addTest("read char", [&] { + suite.test("read char") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -122,11 +122,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('c')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('c'); + }; - suite.addTest("token", [&] { + suite.test("token") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -138,10 +138,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("c"sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("string", [&] { + suite.test("string") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -153,10 +153,10 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("Hello World!"sv, grammarAst); std::println("{}", outputAst); - suite.expect(std::ranges::empty(outputAst.root.nodes)); - }); + expect(std::ranges::empty(outputAst.root.nodes)); + }; - suite.addTest("recurrence", [&] { + suite.test("recurrence") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -169,12 +169,12 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("AAA"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 3uz); + expect(std::ranges::size(outputAst.root.nodes)) == 3uz; for (auto i = 0uz; i < 3uz; ++i) - suite.expectEqual(outputAst.root.nodes[i].value, static_cast('A')); - }); + expect(outputAst.root.nodes[i].value) == static_cast('A'); + }; - suite.addTest("optional", [&] { + suite.test("optional") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -187,12 +187,12 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("AAA"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 3uz); + expect(std::ranges::size(outputAst.root.nodes)) == 3uz; for (auto i = 0uz; i < 3uz; ++i) - suite.expectEqual(outputAst.root.nodes[i].value, static_cast('A')); - }); + expect(outputAst.root.nodes[i].value) == static_cast('A'); + }; - suite.addTest("or", [&] { + suite.test("or") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -203,11 +203,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("BC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('C')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('C'); + }; - suite.addTest("parenthesis", [&] { + suite.test("parenthesis") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -218,11 +218,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("ABC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('C')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('C'); + }; - suite.addTest("comparison", [&] { + suite.test("comparison") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -236,11 +236,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("C"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, "maj"_token); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == "maj"_token; + }; - suite.addTest("hash", [&] { + suite.test("hash") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -252,11 +252,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("variable"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, "variable"_token); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == "variable"_token; + }; - suite.addTest("not", [&] { + suite.test("not") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -269,15 +269,15 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse(R"("Hello")"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 5uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('H')); - suite.expectEqual(outputAst.root.nodes[1].value, static_cast('e')); - suite.expectEqual(outputAst.root.nodes[2].value, static_cast('l')); - suite.expectEqual(outputAst.root.nodes[3].value, static_cast('l')); - suite.expectEqual(outputAst.root.nodes[4].value, static_cast('o')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 5uz; + expect(outputAst.root.nodes[0].value) == static_cast('H'); + expect(outputAst.root.nodes[1].value) == static_cast('e'); + expect(outputAst.root.nodes[2].value) == static_cast('l'); + expect(outputAst.root.nodes[3].value) == static_cast('l'); + expect(outputAst.root.nodes[4].value) == static_cast('o'); + }; - suite.addTest("sub", [&] { + suite.test("sub") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -289,20 +289,20 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("ABC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 2uz); + expect(std::ranges::size(outputAst.root.nodes)) == 2uz; const auto& aNode = outputAst.root.nodes[0]; - suite.expectEqual(aNode.value, static_cast('A')); - suite.expectEqual(std::ranges::size(aNode.nodes), 1uz); + expect(aNode.value) == static_cast('A'); + expect(std::ranges::size(aNode.nodes)) == 1uz; const auto& bNode = aNode.nodes[0]; - suite.expectEqual(bNode.value, static_cast('B')); - suite.expectEqual(std::ranges::size(bNode.nodes), 1uz); + expect(bNode.value) == static_cast('B'); + expect(std::ranges::size(bNode.nodes)) == 1uz; const auto& cNode = bNode.nodes[0]; - suite.expectEqual(cNode.value, static_cast('C')); - suite.expect(std::ranges::empty(cNode.nodes)); - suite.expectEqual(outputAst.root.nodes[1].value, static_cast(';')); - }); + expect(cNode.value) == static_cast('C'); + expect(std::ranges::empty(cNode.nodes)); + expect(outputAst.root.nodes[1].value) == static_cast(';'); + }; - suite.addTest("repeat", [&] { + suite.test("repeat") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -313,11 +313,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("AAAB,B,BC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, static_cast('C')); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == static_cast('C'); + }; - suite.addTest("push-pop", [&] { + suite.test("push-pop") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -329,11 +329,11 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("ABC"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, "ABC"_token); - }); + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == "ABC"_token; + }; - suite.addTest("unsigned int", [&] { + suite.test("unsigned int") = [&] { const auto lowLevelGrammarAst = CppUtils::Language::Parser::parseAst(CppUtils::Language::Lexer::Grammar::lowLevelGrammarSrc); const auto grammarAst = CppUtils::Language::Lexer::parse(R"( @@ -346,8 +346,8 @@ namespace CppUtils::UnitTest::Language::Lexer::Grammar::LowLevelGrammar const auto outputAst = CppUtils::Language::Lexer::parse("42"sv, grammarAst); std::println("{}", outputAst); - suite.expectEqual(std::ranges::size(outputAst.root.nodes), 1uz); - suite.expectEqual(outputAst.root.nodes[0].value, 42uz); - }); - }}; + expect(std::ranges::size(outputAst.root.nodes)) == 1uz; + expect(outputAst.root.nodes[0].value) == 42uz; + }; + }; } diff --git a/tests/Language/Markdown/MarkdownLexer.mpp b/tests/Language/Markdown/MarkdownLexer.mpp index 9d30acb9..d70a424a 100644 --- a/tests/Language/Markdown/MarkdownLexer.mpp +++ b/tests/Language/Markdown/MarkdownLexer.mpp @@ -8,21 +8,21 @@ namespace CppUtils::UnitTest::Language::Markdown::MarkdownLexer using namespace CppUtils::String::Literals; using namespace CppUtils::Language::Markdown::Literals; - auto _ = TestSuite{"Language/Markdown/MarkdownLexer", {}, [](TestSuite& suite) { + auto _ = testSuite("Language/Markdown/MarkdownLexer") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("Minimalist", [&] { + suite.test("Minimalist") = [&] { const auto markdownAst = R"( # Hello World! )"_markdown; CppUtils::Log::TreeNodeLogger::log(markdownAst); - suite.expect(markdownAst.exists("h1"_token)); - suite.expect(markdownAst["h1"_token].exists("content"_token)); - suite.expectEqual(std::get(markdownAst["h1"_token]["content"_token].nodes.at(0).value), "Hello World!"s); - }); + expect(markdownAst.exists("h1"_token)); + expect(markdownAst["h1"_token].exists("content"_token)); + expect(std::get(markdownAst["h1"_token]["content"_token].nodes.at(0).value)) == "Hello World!"; + }; - suite.addTest("Format", [&] { + suite.test("Format") = [&] { const auto markdownAst = R"( *italic* _italic_ @@ -36,12 +36,12 @@ namespace CppUtils::UnitTest::Language::Markdown::MarkdownLexer )"_markdown; CppUtils::Log::TreeNodeLogger::log(markdownAst); - suite.expectEqual(std::ranges::size(markdownAst.nodes), 9uz); - suite.expectEqual(std::ranges::size(markdownAst.nodes.at(0).nodes), 1uz); - suite.expectEqual(std::get(markdownAst.nodes.at(0).value), "italic"_token); - }); + expect(std::ranges::size(markdownAst.nodes)) == 9uz; + expect(std::ranges::size(markdownAst.nodes.at(0).nodes)) == 1uz; + expect(std::get(markdownAst.nodes.at(0).value)) == "italic"_token; + }; - suite.addTest("Title", [&] { + suite.test("Title") = [&] { const auto markdownAst = R"( # H1 ## H2 @@ -52,66 +52,66 @@ namespace CppUtils::UnitTest::Language::Markdown::MarkdownLexer )"_markdown; CppUtils::Log::TreeNodeLogger::log(markdownAst); - suite.expect(markdownAst.exists("h1"_token)); - suite.expect(markdownAst.exists("h2"_token)); - suite.expect(markdownAst.exists("h3"_token)); - suite.expect(markdownAst.exists("h4"_token)); - suite.expect(markdownAst.exists("h5"_token)); - suite.expect(markdownAst.exists("h6"_token)); - }); + expect(markdownAst.exists("h1"_token)); + expect(markdownAst.exists("h2"_token)); + expect(markdownAst.exists("h3"_token)); + expect(markdownAst.exists("h4"_token)); + expect(markdownAst.exists("h5"_token)); + expect(markdownAst.exists("h6"_token)); + }; - suite.addTest("Checkbox", [&] { + suite.test("Checkbox") = [&] { const auto markdownAst = R"( [ ] [x] )"_markdown; CppUtils::Log::TreeNodeLogger::log(markdownAst); - suite.expectEqual(std::get(markdownAst.nodes.at(0).value), "checkbox"_token); - suite.expectEqual(std::get(markdownAst.nodes.at(1).value), "checkbox"_token); - suite.expect(markdownAst.nodes.at(0).nodes.empty()); - suite.expect(markdownAst.nodes.at(1).exists("attributes"_token)); - suite.expectEqual(std::get(markdownAst.nodes.at(1)["attributes"_token].nodes.at(0).value), "checked"_token); - }); + expect(std::get(markdownAst.nodes.at(0).value)) == "checkbox"_token; + expect(std::get(markdownAst.nodes.at(1).value)) == "checkbox"_token; + expect(markdownAst.nodes.at(0).nodes.empty()); + expect(markdownAst.nodes.at(1).exists("attributes"_token)); + expect(std::get(markdownAst.nodes.at(1)["attributes"_token].nodes.at(0).value)) == "checked"_token; + }; - suite.addTest("Image", [&] { + suite.test("Image") = [&] { const auto markdownAst = R"( ![title](url) )"_markdown; CppUtils::Log::TreeNodeLogger::log(markdownAst); - suite.expect(markdownAst.exists("image"_token)); - suite.expect(markdownAst["image"_token].exists("attributes"_token)); + expect(markdownAst.exists("image"_token)); + expect(markdownAst["image"_token].exists("attributes"_token)); const auto& attributes = markdownAst["image"_token]["attributes"_token]; - suite.expectEqual(std::get(attributes["alt"_token].nodes.at(0).value), "title"s); - suite.expectEqual(std::get(attributes["src"_token].nodes.at(0).value), "url"s); - }); + expect(std::get(attributes["alt"_token].nodes.at(0).value)) == "title"; + expect(std::get(attributes["src"_token].nodes.at(0).value)) == "url"; + }; - suite.addTest("Link", [&] { + suite.test("Link") = [&] { const auto markdownAst = R"( [title](url) )"_markdown; CppUtils::Log::TreeNodeLogger::log(markdownAst); - suite.expect(markdownAst.exists("link"_token)); - suite.expect(markdownAst["link"_token].exists("attributes"_token)); + expect(markdownAst.exists("link"_token)); + expect(markdownAst["link"_token].exists("attributes"_token)); const auto& attributes = markdownAst["link"_token]["attributes"_token]; - suite.expectEqual(std::get(attributes["alt"_token].nodes.at(0).value), "title"s); - suite.expectEqual(std::get(attributes["src"_token].nodes.at(0).value), "url"s); - }); + expect(std::get(attributes["alt"_token].nodes.at(0).value)) == "title"; + expect(std::get(attributes["src"_token].nodes.at(0).value)) == "url"; + }; - suite.addTest("Code", [&] { + suite.test("Code") = [&] { const auto markdownAst = R"( `const variable = 42;` )"_markdown; CppUtils::Log::TreeNodeLogger::log(markdownAst); - suite.expect(markdownAst.exists("code"_token)); - suite.expect(markdownAst["code"_token].exists("content"_token)); - suite.expectEqual(std::get(markdownAst["code"_token]["content"_token].nodes.at(0).value), "const variable = 42;"s); - }); + expect(markdownAst.exists("code"_token)); + expect(markdownAst["code"_token].exists("content"_token)); + expect(std::get(markdownAst["code"_token]["content"_token].nodes.at(0).value)) == "const variable = 42;"; + }; - suite.addTest("Blockcode", [&] { + suite.test("Blockcode") = [&] { const auto markdownAst = R"( ```js const variable = 42; @@ -119,13 +119,13 @@ namespace CppUtils::UnitTest::Language::Markdown::MarkdownLexer )"_markdown; CppUtils::Log::TreeNodeLogger::log(markdownAst); - suite.expect(markdownAst.exists("blockcode"_token)); + expect(markdownAst.exists("blockcode"_token)); const auto& blockcode = markdownAst["blockcode"_token]; - suite.expect(blockcode.exists("attributes"_token)); - suite.expect(blockcode["attributes"_token].exists("lang"_token)); - suite.expectEqual(std::get(blockcode["attributes"_token]["lang"_token].nodes.at(0).value), "js"_token); - suite.expect(blockcode.exists("content"_token)); - suite.expectEqual(std::get(blockcode["content"_token].nodes.at(0).value), "const variable = 42;"s); - }); - }}; + expect(blockcode.exists("attributes"_token)); + expect(blockcode["attributes"_token].exists("lang"_token)); + expect(std::get(blockcode["attributes"_token]["lang"_token].nodes.at(0).value)) == "js"_token; + expect(blockcode.exists("content"_token)); + expect(std::get(blockcode["content"_token].nodes.at(0).value)) == "const variable = 42;"; + }; + }; } diff --git a/tests/Language/MetaCircularParser.mpp b/tests/Language/MetaCircularParser.mpp index 71219148..24f1212b 100644 --- a/tests/Language/MetaCircularParser.mpp +++ b/tests/Language/MetaCircularParser.mpp @@ -5,17 +5,14 @@ import CppUtils; namespace CppUtils::UnitTest::Language::MetaCircularParser { - auto _ = TestSuite{ - "Language/MetaCircularParser", - {"Logger", "Container/MeshNetwork"}, - [](TestSuite& suite) { + auto _ = testSuite("Language/MetaCircularParser").dependsOn("Logger", "Container/MeshNetwork") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::String::Literals; using Logger = TestSuite::Logger; using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; - suite.addTest("Length function", [&] { + suite.test("Length function") = [&] { auto interpreter = CppUtils::Language::MetaCircularParser{"Hello"sv}; auto lengthNode = MeshNodePtr::makeRoot(0uz); @@ -26,11 +23,11 @@ namespace CppUtils::UnitTest::Language::MetaCircularParser if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(lengthNode.getValue().value(), 5uz); - }); + expect(status); + expect(lengthNode.getValue().value()) == 5uz; + }; - suite.addTest("Read function", [&] { + suite.test("Read function") = [&] { auto interpreter = CppUtils::Language::MetaCircularParser{"ABC"sv}; auto charNode = MeshNodePtr::makeRoot(0uz); auto positionNode = MeshNodePtr::make(0uz); @@ -44,8 +41,8 @@ namespace CppUtils::UnitTest::Language::MetaCircularParser if (not result) Logger::print<"error">("Error: {}", result.error()); - suite.expect(result); - suite.expectEqual(charNode.getValue().value(), static_cast('A')); + expect(result); + expect(charNode.getValue().value()) == static_cast('A'); } positionNode.setValue(1uz); { @@ -53,12 +50,12 @@ namespace CppUtils::UnitTest::Language::MetaCircularParser if (not result) Logger::print<"error">("Error: {}", result.error()); - suite.expect(result); - suite.expectEqual(charNode.getValue().value(), static_cast('B')); + expect(result); + expect(charNode.getValue().value()) == static_cast('B'); } - }); + }; - suite.addTest("Read at end of input", [&] { + suite.test("Read at end of input") = [&] { auto interpreter = CppUtils::Language::MetaCircularParser{"A"sv}; auto readInstruction = MeshNodePtr::makeRoot("read"_token); @@ -66,11 +63,11 @@ namespace CppUtils::UnitTest::Language::MetaCircularParser readInstruction["position"_token] >> MeshNodePtr::make(1uz); auto result = interpreter(readInstruction); - suite.expect(not result); - suite.expectEqual(result.error(), "read: End of input"sv); - }); + expect(not result); + expect(result.error()) == "read: End of input"; + }; - suite.addTest("Iterative hash", [&] { + suite.test("Iterative hash") = [&] { constexpr auto source = "TestString"sv; auto interpreter = CppUtils::Language::MetaCircularParser{source}; @@ -112,13 +109,13 @@ namespace CppUtils::UnitTest::Language::MetaCircularParser if (not result) Logger::print<"error">("Error: {}", result.error()); - suite.expect(result); + expect(result); } - suite.expectEqual(currentHashNode.getValue().value(), CppUtils::Type::hash(source)); - }); + expect(currentHashNode.getValue().value()) == CppUtils::Type::hash(source); + }; - suite.addTest("Iterative hash function", [&] { + suite.test("Iterative hash function") = [&] { constexpr auto source = "TestString"sv; auto interpreter = CppUtils::Language::MetaCircularParser{source}; @@ -126,13 +123,13 @@ namespace CppUtils::UnitTest::Language::MetaCircularParser if (not result) Logger::print<"error">("Error: {}", result.error()); - suite.expect(result); + expect(result); auto parseToken = interpreter.get("parseToken"_token); - suite.expect(parseToken); - suite.expectEqual(parseToken.value()["result"_token].front()->getValue().value(), CppUtils::Type::hash(source)); - }); + expect(parseToken); + expect(parseToken.value()["result"_token].front()->getValue().value()) == CppUtils::Type::hash(source); + }; - suite.addTest("Set function", [&] { + suite.test("Set function") = [&] { auto interpreter = CppUtils::Language::MetaCircularParser{""sv}; auto targetNode = MeshNodePtr::makeRoot(0uz); auto instruction = MeshNodePtr::makeRoot("set"_token); @@ -140,8 +137,8 @@ namespace CppUtils::UnitTest::Language::MetaCircularParser instruction["value"_token] >> MeshNodePtr::make(42uz); auto result = interpreter(instruction); - suite.expect(result); - suite.expectEqual(targetNode.getValue().value(), 42uz); - }); - }}; + expect(result); + expect(targetNode.getValue().value()) == 42uz; + }; + }; } diff --git a/tests/Language/MetaCircularVirtualMachine.mpp b/tests/Language/MetaCircularVirtualMachine.mpp index b87b2c5a..eb04619d 100644 --- a/tests/Language/MetaCircularVirtualMachine.mpp +++ b/tests/Language/MetaCircularVirtualMachine.mpp @@ -6,188 +6,185 @@ import CppUtils; namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine { -#if not defined(_MSC_VER) - static_assert([] { + auto _ = testSuite("Language/MetaCircularVirtualMachine").dependsOn("Logger", "Container/MeshNetwork") = [](TestSuite& suite) { + using namespace std::literals; using namespace CppUtils::String::Literals; + using Logger = TestSuite::Logger; using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; - auto interpreter = MetaCircularVirtualMachine{}; - auto target = MeshNodePtr::makeRoot(0uz); +#if not defined(_MSC_VER) + suite.test<[] { + using namespace CppUtils::String::Literals; + using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; + using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; - auto setInstruction = MeshNodePtr::makeRoot("set"_token); - setInstruction["target"_token] >> target; - setInstruction["value"_token] >> MeshNodePtr::make(10uz); + auto interpreter = MetaCircularVirtualMachine{}; + auto target = MeshNodePtr::makeRoot(0uz); - auto addInstruction = MeshNodePtr::makeRoot("+"_token); - addInstruction["lhs"_token] >> target; - addInstruction["rhs"_token] >> MeshNodePtr::make(32uz); - addInstruction["result"_token] >> target; + auto setInstruction = MeshNodePtr::makeRoot("set"_token); + setInstruction["target"_token] >> target; + setInstruction["value"_token] >> MeshNodePtr::make(10uz); - setInstruction["next"_token] >> addInstruction; + auto addInstruction = MeshNodePtr::makeRoot("+"_token); + addInstruction["lhs"_token] >> target; + addInstruction["rhs"_token] >> MeshNodePtr::make(32uz); + addInstruction["result"_token] >> target; - auto status = interpreter(setInstruction); - if (not status) - return false; + setInstruction["next"_token] >> addInstruction; - return target.getValue().value() == 42uz; - }()); + auto status = interpreter(setInstruction); + if (not status) + return false; - static_assert([] { - using namespace CppUtils::String::Literals; - using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; - using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; + return target.getValue().value() == 42uz; + }>("Constexpr set and add instructions"); - auto interpreter = MetaCircularVirtualMachine{}; - auto value = MeshNodePtr::makeRoot("root"_token); + suite.test<[] { + using namespace CppUtils::String::Literals; + using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; + using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; - auto createNode = MeshNodePtr::makeRoot("create"_token); - createNode["value"_token] >> MeshNodePtr::make(1uz); - createNode["destination"_token] >> value; - createNode["branch"_token] >> MeshNodePtr::make("value"_token); + auto interpreter = MetaCircularVirtualMachine{}; + auto value = MeshNodePtr::makeRoot("root"_token); - const auto status = interpreter(createNode); - if (not status) - return false; + auto createNode = MeshNodePtr::makeRoot("create"_token); + createNode["value"_token] >> MeshNodePtr::make(1uz); + createNode["destination"_token] >> value; + createNode["branch"_token] >> MeshNodePtr::make("value"_token); - auto valueNode = value["value"_token].front(); - if (not valueNode) - return false; + const auto status = interpreter(createNode); + if (not status) + return false; - return valueNode->getValue().value() == 1uz; - }()); + auto valueNode = value["value"_token].front(); + if (not valueNode) + return false; - static_assert([] { - using namespace CppUtils::String::Literals; - using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; - using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; + return valueNode->getValue().value() == 1uz; + }>("Constexpr create node instruction"); - auto interpreter = MetaCircularVirtualMachine{}; - auto resultNode = MeshNodePtr::make(false); + suite.test<[] { + using namespace CppUtils::String::Literals; + using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; + using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; - auto equalInstruction = MeshNodePtr::makeRoot("=="_token); - equalInstruction["lhs"_token] >> MeshNodePtr::make(42uz); - equalInstruction["rhs"_token] >> MeshNodePtr::make(42uz); - equalInstruction["result"_token] >> resultNode; + auto interpreter = MetaCircularVirtualMachine{}; + auto resultNode = MeshNodePtr::make(false); - auto status = interpreter(equalInstruction); - if (not status) - return false; + auto equalInstruction = MeshNodePtr::makeRoot("=="_token); + equalInstruction["lhs"_token] >> MeshNodePtr::make(42uz); + equalInstruction["rhs"_token] >> MeshNodePtr::make(42uz); + equalInstruction["result"_token] >> resultNode; - return resultNode.getValue().value() == true; - }()); + auto status = interpreter(equalInstruction); + if (not status) + return false; - static_assert([] { - using namespace CppUtils::String::Literals; - using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; - using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; + return resultNode.getValue().value() == true; + }>("Constexpr equal comparison instruction"); - auto interpreter = MetaCircularVirtualMachine{}; - auto value = MeshNodePtr::make(true); + suite.test<[] { + using namespace CppUtils::String::Literals; + using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; + using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; - auto notInstruction = MeshNodePtr::makeRoot("not"_token); - notInstruction["value"_token] >> value; + auto interpreter = MetaCircularVirtualMachine{}; + auto value = MeshNodePtr::make(true); - auto status = interpreter(notInstruction); - if (not status) - return false; + auto notInstruction = MeshNodePtr::makeRoot("not"_token); + notInstruction["value"_token] >> value; - return static_cast(value.getValue().value()) == false; - }()); + auto status = interpreter(notInstruction); + if (not status) + return false; - static_assert([] { - using namespace CppUtils::String::Literals; - using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; - using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; + return static_cast(value.getValue().value()) == false; + }>("Constexpr not instruction"); - auto interpreter = MetaCircularVirtualMachine{}; - auto value = MeshNodePtr::makeRoot(0uz); + suite.test<[] { + using namespace CppUtils::String::Literals; + using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; + using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; - auto mainNode = interpreter.context["main"_token] >> MeshNodePtr::make("main"_token); + auto interpreter = MetaCircularVirtualMachine{}; + auto value = MeshNodePtr::makeRoot(0uz); - auto jumpNode = mainNode["next"_token] >> MeshNodePtr::make("jump"_token); + auto mainNode = interpreter.context["main"_token] >> MeshNodePtr::make("main"_token); - auto create1 = jumpNode["else"_token] >> MeshNodePtr::make("create"_token); - create1["value"_token] >> MeshNodePtr::make(1uz); - create1["destination"_token] >> value; - create1["branch"_token] >> MeshNodePtr::make("value"_token); + auto jumpNode = mainNode["next"_token] >> MeshNodePtr::make("jump"_token); - auto create2 = jumpNode["next"_token] >> MeshNodePtr::make("create"_token); - create2["value"_token] >> MeshNodePtr::make(2uz); - create2["destination"_token] >> value; - create2["branch"_token] >> MeshNodePtr::make("value"_token); + auto create1 = jumpNode["else"_token] >> MeshNodePtr::make("create"_token); + create1["value"_token] >> MeshNodePtr::make(1uz); + create1["destination"_token] >> value; + create1["branch"_token] >> MeshNodePtr::make("value"_token); - jumpNode["condition"_token] >> MeshNodePtr::make(1uz); + auto create2 = jumpNode["next"_token] >> MeshNodePtr::make("create"_token); + create2["value"_token] >> MeshNodePtr::make(2uz); + create2["destination"_token] >> value; + create2["branch"_token] >> MeshNodePtr::make("value"_token); - const auto status = interpreter("main"_token); - if (not status) - return false; + jumpNode["condition"_token] >> MeshNodePtr::make(1uz); - auto valueNode = value["value"_token].front(); - if (not valueNode) - return false; + const auto status = interpreter("main"_token); + if (not status) + return false; - return valueNode->getValue().value() == 2uz; - }()); + auto valueNode = value["value"_token].front(); + if (not valueNode) + return false; - static_assert([] { - using namespace CppUtils::String::Literals; - using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; - using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; + return valueNode->getValue().value() == 2uz; + }>("Constexpr jump instruction"); - auto interpreter = MetaCircularVirtualMachine{}; - - auto lhs = MeshNodePtr::make(2uz); - auto rhs = MeshNodePtr::make(3uz); - auto result = MeshNodePtr::make(0uz); - - auto call = MeshNodePtr::makeRoot("call"_token); - auto inputNode = call["input"_token] >> MeshNodePtr::make("inputs"_token); - inputNode["lhs"_token] >> lhs; - inputNode["rhs"_token] >> rhs; - auto outputNode = call["output"_token] >> MeshNodePtr::make("outputs"_token); - outputNode["result"_token] >> result; - - auto referenceLhs = call["function"_token] >> MeshNodePtr::make("reference"_token); - referenceLhs["sourcePath"_token] >> MeshNodePtr::make("input"_token); - referenceLhs["sourcePath"_token] >> MeshNodePtr::make("lhs"_token); - referenceLhs["destinationPath"_token] >> MeshNodePtr::make("lhs"_token); - auto addInstruction = referenceLhs["destinationBase"_token] >> MeshNodePtr::make("+"_token); - - auto referenceRhs = referenceLhs["next"_token] >> MeshNodePtr::make("reference"_token); - referenceRhs["sourcePath"_token] >> MeshNodePtr::make("input"_token); - referenceRhs["sourcePath"_token] >> MeshNodePtr::make("rhs"_token); - referenceRhs["destinationPath"_token] >> MeshNodePtr::make("rhs"_token); - referenceRhs["destinationBase"_token] >> addInstruction; - - auto referenceResult = referenceRhs["next"_token] >> MeshNodePtr::make("reference"_token); - referenceResult["sourcePath"_token] >> MeshNodePtr::make("output"_token); - referenceResult["sourcePath"_token] >> MeshNodePtr::make("result"_token); - referenceResult["destinationPath"_token] >> MeshNodePtr::make("result"_token); - referenceResult["destinationBase"_token] >> addInstruction; - referenceResult["next"_token] >> addInstruction; - - const auto status = interpreter(call); - if (not status) - return false; - - return result.getValue().value() == 5uz; - }()); -#endif + suite.test<[] { + using namespace CppUtils::String::Literals; + using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; + using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; - auto _ = TestSuite{ - "Language/MetaCircularVirtualMachine", - {"Logger", "Container/MeshNetwork"}, - [](TestSuite& suite) { - using namespace std::literals; - using namespace CppUtils::String::Literals; - using Logger = TestSuite::Logger; - using MetaCircularVirtualMachine = CppUtils::Language::MetaCircularVirtualMachine; - using MeshNodePtr = MetaCircularVirtualMachine::MeshNodePtr; + auto interpreter = MetaCircularVirtualMachine{}; + + auto lhs = MeshNodePtr::make(2uz); + auto rhs = MeshNodePtr::make(3uz); + auto result = MeshNodePtr::make(0uz); + + auto call = MeshNodePtr::makeRoot("call"_token); + auto inputNode = call["input"_token] >> MeshNodePtr::make("inputs"_token); + inputNode["lhs"_token] >> lhs; + inputNode["rhs"_token] >> rhs; + auto outputNode = call["output"_token] >> MeshNodePtr::make("outputs"_token); + outputNode["result"_token] >> result; + + auto referenceLhs = call["function"_token] >> MeshNodePtr::make("reference"_token); + referenceLhs["sourcePath"_token] >> MeshNodePtr::make("input"_token); + referenceLhs["sourcePath"_token] >> MeshNodePtr::make("lhs"_token); + referenceLhs["destinationPath"_token] >> MeshNodePtr::make("lhs"_token); + auto addInstruction = referenceLhs["destinationBase"_token] >> MeshNodePtr::make("+"_token); + + auto referenceRhs = referenceLhs["next"_token] >> MeshNodePtr::make("reference"_token); + referenceRhs["sourcePath"_token] >> MeshNodePtr::make("input"_token); + referenceRhs["sourcePath"_token] >> MeshNodePtr::make("rhs"_token); + referenceRhs["destinationPath"_token] >> MeshNodePtr::make("rhs"_token); + referenceRhs["destinationBase"_token] >> addInstruction; + + auto referenceResult = referenceRhs["next"_token] >> MeshNodePtr::make("reference"_token); + referenceResult["sourcePath"_token] >> MeshNodePtr::make("output"_token); + referenceResult["sourcePath"_token] >> MeshNodePtr::make("result"_token); + referenceResult["destinationPath"_token] >> MeshNodePtr::make("result"_token); + referenceResult["destinationBase"_token] >> addInstruction; + referenceResult["next"_token] >> addInstruction; + + const auto status = interpreter(call); + if (not status) + return false; + + return result.getValue().value() == 5uz; + }>("Constexpr call and references instruction"); +#endif - suite.addTest("Canary: MSVC STL std::optional non-trivial destructor in constant evaluation", [&] { + suite.test("Canary: MSVC STL std::optional non-trivial destructor in constant evaluation") = [] { #if defined(_MSC_VER) - suite.skip("Waiting for MSVC STL issue fix: https://github.com/microsoft/STL/issues/6467"); + skip("Waiting for MSVC STL issue fix: https://github.com/microsoft/STL/issues/6467"); #else constexpr auto passed = [] { auto interpreter = MetaCircularVirtualMachine{}; @@ -210,18 +207,18 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine return target.getValue().value() == 42uz; }(); - suite.expect(passed); + expect(passed); #endif - }); + }; - suite.addTest("Check if main function exists", [&] { + suite.test("Check if main function exists") = [&] { auto interpreter = MetaCircularVirtualMachine{}; - suite.expect(not interpreter.get("main"_token)); + expect(not interpreter.get("main"_token)); interpreter.context["main"_token] >> MeshNodePtr::make("main"_token); - suite.expect(interpreter.get("main"_token)); - }); + expect(interpreter.get("main"_token)); + }; - suite.addTest("Create node", [&] { + suite.test("Create node") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto value = MeshNodePtr::makeRoot("root"_token); @@ -234,13 +231,13 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto valueNode = value["value"_token].front(); - suite.expect(valueNode); - suite.expectEqual(valueNode->getValue().value(), 1uz); - }); + expect(valueNode); + expect(valueNode->getValue().value()) == 1uz; + }; - suite.addTest("Internal function call", [&] { + suite.test("Internal function call") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto value = MeshNodePtr::makeRoot(0uz); @@ -254,13 +251,13 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto valueNode = value["value"_token].front(); - suite.expect(valueNode); - suite.expectEqual(valueNode->getValue().value(), 1uz); - }); + expect(valueNode); + expect(valueNode->getValue().value()) == 1uz; + }; - suite.addTest("External function call", [&] { + suite.test("External function call") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto called = false; @@ -273,11 +270,11 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expect(called); - }); + expect(status); + expect(called); + }; - suite.addTest("resolve: vertical resolution", [&] { + suite.test("resolve: vertical resolution") = [&] { auto interpreter = MetaCircularVirtualMachine{}; interpreter.context["x"_token] >> MeshNodePtr::make(10uz); @@ -292,15 +289,15 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine childInterpreter.environment = interpreter.environment; auto yNode = childInterpreter.resolve("y"_token); - suite.expect(yNode); - suite.expectEqual(yNode->getValue().value(), 20uz); + expect(yNode); + expect(yNode->getValue().value()) == 20uz; auto xNode = childInterpreter.resolve("x"_token); - suite.expect(xNode); - suite.expectEqual(xNode->getValue().value(), 10uz); - }); + expect(xNode); + expect(xNode->getValue().value()) == 10uz; + }; - suite.addTest("resolve: horizontal resolution", [&] { + suite.test("resolve: horizontal resolution") = [&] { auto interpreter = MetaCircularVirtualMachine{}; interpreter.context["a"_token] >> MeshNodePtr::make("b"_token); @@ -308,12 +305,12 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine auto nodeC = interpreter.context["c"_token] >> MeshNodePtr::make(42uz); auto result = interpreter.resolve("a"_token); - suite.expect(result); - suite.expectEqual(result->getValue().value(), 42uz); - suite.expect(result.value() == nodeC); - }); + expect(result); + expect(result->getValue().value()) == 42uz; + expect(result.value()) == nodeC; + }; - suite.addTest("resolve: mixed vertical and horizontal resolution", [&] { + suite.test("resolve: mixed vertical and horizontal resolution") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto nodeY = interpreter.context["y"_token] >> MeshNodePtr::make(123uz); @@ -327,12 +324,12 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine childInterpreter.environment = interpreter.environment; auto result = childInterpreter.resolve("x"_token); - suite.expect(result); - suite.expectEqual(result->getValue().value(), 123uz); - suite.expect(result.value() == nodeY); - }); + expect(result); + expect(result->getValue().value()) == 123uz; + expect(result.value()) == nodeY; + }; - suite.addTest("Link", [&] { + suite.test("Link") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto target = MeshNodePtr::makeRoot("Target"_token); auto child = MeshNodePtr::makeRoot("Child"_token); @@ -346,18 +343,18 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expect(target.contains("Branch"_token)); - suite.expectEqual(std::ranges::size(target.at("Branch"_token).value()), 1uz); - suite.expectEqual(target.at("Branch"_token)->front()->getValue().value(), "Child"_token); - }); + expect(status); + expect(target.contains("Branch"_token)); + expect(std::ranges::size(target.at("Branch"_token).value())) == 1uz; + expect(target.at("Branch"_token)->front()->getValue().value()) == "Child"_token; + }; - suite.addTest("Unlink", [&] { + suite.test("Unlink") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto target = MeshNodePtr::makeRoot("Target"_token); auto child = MeshNodePtr::makeRoot("Child"_token); target["Branch"_token] >> child; - suite.expect(target.contains("Branch"_token)); + expect(target.contains("Branch"_token)); // unlink branch { @@ -369,14 +366,14 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expect(not target.contains("Branch"_token)); + expect(status); + expect(not target.contains("Branch"_token)); } // unlink single node { target["Branch"_token] >> child; - suite.expect(target.contains("Branch"_token)); + expect(target.contains("Branch"_token)); auto unlinkNode = MeshNodePtr::makeRoot("unlink"_token); unlinkNode["source"_token] >> target; @@ -387,12 +384,12 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expect(not target.contains("Branch"_token)); + expect(status); + expect(not target.contains("Branch"_token)); } - }); + }; - suite.addTest("Equality (==)", [&] { + suite.test("Equality (==)") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto rhs = MeshNodePtr::make(42uz); auto resultNode = MeshNodePtr::make(false); @@ -407,8 +404,8 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(resultNode.getValue().value(), true); + expect(status); + expect(resultNode.getValue().value()); } rhs.setValue(43uz); { @@ -416,12 +413,12 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(resultNode.getValue().value(), false); + expect(status); + expect(resultNode.getValue().value()) == false; } - }); + }; - suite.addTest("Inversion (not)", [&] { + suite.test("Inversion (not)") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto value = MeshNodePtr::make(true); @@ -433,8 +430,8 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(static_cast(value.getValue().value()), false); + expect(status); + expect(static_cast(value.getValue().value())) == false; } { @@ -442,12 +439,12 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(static_cast(value.getValue().value()), true); + expect(status); + expect(static_cast(value.getValue().value())); } - }); + }; - suite.addTest("Jump", [&] { + suite.test("Jump") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto value = MeshNodePtr::makeRoot(0uz); @@ -471,13 +468,13 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto valueNode = value["value"_token].front(); - suite.expect(valueNode); - suite.expectEqual(valueNode->getValue().value(), 2uz); - }); + expect(valueNode); + expect(valueNode->getValue().value()) == 2uz; + }; - suite.addTest("Return", [&] { + suite.test("Return") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto value = MeshNodePtr::makeRoot(0uz); @@ -499,13 +496,13 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto valueNode = value["value"_token].front(); - suite.expect(valueNode); - suite.expectEqual(valueNode->getValue().value(), 1uz); - }); + expect(valueNode); + expect(valueNode->getValue().value()) == 1uz; + }; - suite.addTest("Addition (+)", [&] { + suite.test("Addition (+)") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto result = MeshNodePtr::make(0uz); @@ -518,11 +515,11 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(result.getValue().value(), 15uz); - }); + expect(status); + expect(result.getValue().value()) == 15uz; + }; - suite.addTest("Subtraction (-)", [&] { + suite.test("Subtraction (-)") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto result = MeshNodePtr::make(0uz); @@ -535,11 +532,11 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(result.getValue().value(), 7uz); - }); + expect(status); + expect(result.getValue().value()) == 7uz; + }; - suite.addTest("Multiplication (*)", [&] { + suite.test("Multiplication (*)") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto result = MeshNodePtr::make(0uz); @@ -552,11 +549,11 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(result.getValue().value(), 20uz); - }); + expect(status); + expect(result.getValue().value()) == 20uz; + }; - suite.addTest("Division (/)", [&] { + suite.test("Division (/)") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto result = MeshNodePtr::make(0uz); @@ -569,11 +566,11 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(result.getValue().value(), 5uz); - }); + expect(status); + expect(result.getValue().value()) == 5uz; + }; - suite.addTest("Modulus (%)", [&] { + suite.test("Modulus (%)") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto result = MeshNodePtr::make(0uz); @@ -586,11 +583,11 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(result.getValue().value(), 2uz); - }); + expect(status); + expect(result.getValue().value()) == 2uz; + }; - suite.addTest("Less than (<)", [&] { + suite.test("Less than (<)") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto result = MeshNodePtr::make(false); @@ -604,8 +601,8 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(result.getValue().value(), true); + expect(status); + expect(result.getValue().value()); } { @@ -618,12 +615,12 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(result.getValue().value(), false); + expect(status); + expect(result.getValue().value()) == false; } - }); + }; - suite.addTest("reference: link two nodes", [&] { + suite.test("reference: link two nodes") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto sourceNode = MeshNodePtr::makeRoot("source"_token); @@ -637,14 +634,14 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine const auto status = interpreter(instruction); if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto resolved = interpreter.get("myReference"_token); - suite.expect(resolved); - suite.expectEqual(resolved->getValue().value(), 123uz); - }); + expect(resolved); + expect(resolved->getValue().value()) == 123uz; + }; - suite.addTest("reference: deep destination with auto-vivification", [&] { + suite.test("reference: deep destination with auto-vivification") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto sourceNode = MeshNodePtr::makeRoot("source"_token); @@ -660,18 +657,18 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine const auto status = interpreter(instruction); if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto path = interpreter.context["path"_token].front(); - suite.expect(path); + expect(path); auto to = path.value()["to"_token].front(); - suite.expect(to); + expect(to); auto valueNode = to.value()["value"_token].front(); - suite.expect(valueNode); - suite.expectEqual(valueNode->getValue().value(), 42uz); - }); + expect(valueNode); + expect(valueNode->getValue().value()) == 42uz; + }; - suite.addTest("reference: deep source and deep destination", [&] { + suite.test("reference: deep source and deep destination") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto sourceRoot = MeshNodePtr::makeRoot("sourceRoot"_token); @@ -693,16 +690,16 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine const auto status = interpreter(instruction); if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto destination1 = interpreter.get("destination1"_token); - suite.expect(destination1); + expect(destination1); auto destination2 = destination1.value()["destination2"_token].front(); - suite.expect(destination2); - suite.expectEqual(destination2->getValue().value(), 123uz); - }); + expect(destination2); + expect(destination2->getValue().value()) == 123uz; + }; - suite.addTest("set: update node value", [&] { + suite.test("set: update node value") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto targetNode = MeshNodePtr::makeRoot(0uz); @@ -713,12 +710,12 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine const auto status = interpreter(instruction); if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); - suite.expectEqual(targetNode.getValue().value(), 1uz); - }); + expect(targetNode.getValue().value()) == 1uz; + }; - suite.addTest("size: get branch size", [&] { + suite.test("size: get branch size") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto root = MeshNodePtr::makeRoot("root"_token); root["list"_token] >> MeshNodePtr::make(1uz); @@ -735,12 +732,12 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine const auto status = interpreter(instruction); if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); - suite.expectEqual(resultNode.getValue().value(), 3uz); - }); + expect(resultNode.getValue().value()) == 3uz; + }; - suite.addTest("call: no arguments", [&] { + suite.test("call: no arguments") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto value = MeshNodePtr::makeRoot("root"_token); @@ -756,13 +753,13 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto valueNode = value["value"_token].front(); - suite.expect(valueNode); - suite.expectEqual(valueNode->getValue().value(), 42uz); - }); + expect(valueNode); + expect(valueNode->getValue().value()) == 42uz; + }; - suite.addTest("call: input and output", [&] { + suite.test("call: input and output") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto lhs = MeshNodePtr::make(2uz); @@ -799,11 +796,11 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(result.getValue().value(), 5uz); - }); + expect(status); + expect(result.getValue().value()) == 5uz; + }; - suite.addTest("Multithreading - thread and join", [&] { + suite.test("Multithreading - thread and join") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto value = MeshNodePtr::makeRoot("root"_token); @@ -825,13 +822,13 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto valueNode = value["value"_token].front(); - suite.expect(valueNode); - suite.expectEqual(valueNode->getValue().value(), 42uz); - }); + expect(valueNode); + expect(valueNode->getValue().value()) == 42uz; + }; - suite.addTest("Multithreading - event, wait, notify", [&] { + suite.test("Multithreading - event, wait, notify") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto value = MeshNodePtr::makeRoot("root"_token); auto eventIdNode = MeshNodePtr::make(0uz); @@ -862,13 +859,13 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); + expect(status); auto valueNode = value["value"_token].front(); - suite.expect(valueNode); - suite.expectEqual(valueNode->getValue().value(), 42uz); - }); + expect(valueNode); + expect(valueNode->getValue().value()) == 42uz; + }; - suite.addTest("thread: input and output", [&] { + suite.test("thread: input and output") = [&] { auto interpreter = MetaCircularVirtualMachine{}; auto lhs = MeshNodePtr::make(2uz); @@ -915,8 +912,8 @@ namespace CppUtils::UnitTest::Language::MetaCircularVirtualMachine if (not status) Logger::print<"error">("Error: {}", status.error()); - suite.expect(status); - suite.expectEqual(result.getValue().value(), 5uz); - }); - }}; + expect(status); + expect(result.getValue().value()) == 5uz; + }; + }; } diff --git a/tests/Language/Parameters/ParametersLexer.mpp b/tests/Language/Parameters/ParametersLexer.mpp index a00cffe2..89113334 100644 --- a/tests/Language/Parameters/ParametersLexer.mpp +++ b/tests/Language/Parameters/ParametersLexer.mpp @@ -11,24 +11,36 @@ namespace CppUtils::UnitTest::Language::Parameters::ParametersLexer std::string parameter; }; - auto _ = TestSuite{"Language/Parameters/ParametersLexer", {}, [](TestSuite& suite) { + auto _ = testSuite("Language/Parameters/ParametersLexer") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("parseParameters", [&] { - const char* const argv[] = {"executable", "A[aaa]", "B[", "0", "]", "C", "DDD[]", "E[e e e ]"}; + suite.test("parseParameters") = [&] { + const char* const argv[] = {"executable", "--key1=value1", R"(--key2="hello world")", "--key3", "-k=shortValue", "-f"}; const auto parametersLexer = CppUtils::Language::Parameters::ParametersLexer{}; - const auto parameters = parametersLexer.parseParameters(8, argv); - - suite.expectEqual(std::ranges::size(parameters), 5uz); - suite.expectEqual(parameters.at("A"), "aaa"s); - suite.expectEqual(parameters.at("B"), "0"s); - suite.expectEqual(parameters.at("C"), ""s); - suite.expectEqual(parameters.at("DDD"), ""s); - suite.expectEqual(parameters.at("E"), "e e e"s); - }); - - suite.addTest("executeCommands", [&] { - const char* const argv[] = {"executable", "info", "verbose", "parameter[value]"}; + const auto parameters = parametersLexer.parseParameters(6, argv); + + expect(std::ranges::size(parameters)) == 5uz; + expect(parameters.at("key1")) == "value1"; + expect(parameters.at("key2")) == "hello world"; + expect(parameters.at("key3")) == ""; + expect(parameters.at("k")) == "shortValue"; + expect(parameters.at("f")) == ""; + }; + + suite.test("parseParameters with CLI flags") = [&] { + const char* const argv[] = {"executable", "--filter=Network", "--report=junit:build/report.xml", "--fail-fast", "-v"}; + const auto parametersLexer = CppUtils::Language::Parameters::ParametersLexer{}; + const auto parameters = parametersLexer.parseParameters(5, argv); + + expect(std::ranges::size(parameters)) == 4uz; + expect(parameters.at("filter")) == "Network"; + expect(parameters.at("report")) == "junit:build/report.xml"; + expect(parameters.at("fail-fast")) == ""; + expect(parameters.at("v")) == ""; + }; + + suite.test("executeCommands") = [&] { + const char* const argv[] = {"executable", "--info", "--verbose", "--parameter=value"}; auto settings = ProgramSettings{}; const auto parametersLexer = CppUtils::Language::Parameters::ParametersLexer{}; const auto abort = parametersLexer.executeCommands(4, argv, {{"info", []([[maybe_unused]] auto value) -> bool { @@ -42,9 +54,9 @@ namespace CppUtils::UnitTest::Language::Parameters::ParametersLexer settings.parameter = value; return false; }}}); - suite.expectEqual(abort, false); - suite.expectEqual(settings.verbose, true); - suite.expectEqual(settings.parameter, "value"s); - }); - }}; + not expect(abort); + expect(settings.verbose); + expect(settings.parameter) == "value"; + }; + }; } diff --git a/tests/Language/VirtualMachine.mpp b/tests/Language/VirtualMachine.mpp index 74617cd6..3ec15c28 100644 --- a/tests/Language/VirtualMachine.mpp +++ b/tests/Language/VirtualMachine.mpp @@ -5,86 +5,83 @@ import CppUtils; namespace CppUtils::UnitTest::Language::VirtualMachine { - auto _ = TestSuite{ - "Language/VirtualMachine", - {"Logger", "Container/Stack"}, - [](TestSuite& suite) { + auto _ = testSuite("Language/VirtualMachine").dependsOn("Logger", "Container/Stack") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; namespace VM = CppUtils::Language::VirtualMachine; - suite.addTest("empty source", [&] { + suite.test("empty source") = [&] { constexpr auto source = u8""sv; auto result = VM::execute(source); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("push-pop size_t", [&] { + suite.test("push-pop size_t") = [&] { constexpr auto source = u8",[,],"sv; auto result = VM::execute(source); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("push-pop bool", [&] { + suite.test("push-pop bool") = [&] { constexpr auto source = u8",[0:,[0_,"sv; auto result = VM::execute(source); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("push-pop char", [&] { + suite.test("push-pop char") = [&] { constexpr auto source = u8",[0:,[0_,"sv; auto result = VM::execute(source); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("push-pop int", [&] { + suite.test("push-pop int") = [&] { constexpr auto source = u8",[0:,[0_,"sv; auto result = VM::execute(source); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("import int", [&] { + suite.test("import int") = [&] { constexpr auto source = u8R"(,[0",)"sv; auto number = 42; auto result = VM::execute(source, VM::NoFunctions{}, VM::makeReferences(number)); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("input output (int)", [&] { + suite.test("input output (int)") = [&] { constexpr auto source = u8R"([0" [1^ [0~)"sv; auto input = 42; auto output = 0; auto result = VM::execute(source, VM::NoFunctions{}, VM::makeReferences(input, output)); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - suite.expectEqual(output, input); - }); + expect(result); + expect(output) == input; + }; - suite.addTest("input output (char)", [&] { + suite.test("input output (char)") = [&] { constexpr auto source = u8R"([0" [1^ [0~)"sv; auto input = 'A'; auto output = char{}; auto result = VM::execute(source, VM::NoFunctions{}, VM::makeReferences(input, output)); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - suite.expectEqual(output, input); - }); + expect(result); + expect(output) == input; + }; - suite.addTest("Call function: () -> std::size_t", [&] { + suite.test("Call function: () -> std::size_t") = [&] { constexpr auto source = u8R"( [ ()[0; / number = getNumber(); [0^] / output = number; @@ -96,11 +93,11 @@ namespace CppUtils::UnitTest::Language::VirtualMachine auto result = VM::execute(source, VM::Functions{getNumber}, VM::makeReferences(output)); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - suite.expectEqual(output, 42uz); - }); + expect(result); + expect(output) == 42uz; + }; - suite.addTest("Call function: (std::size_t) -> void", [&] { + suite.test("Call function: (std::size_t) -> void") = [&] { constexpr auto source = u8R"( [42 ()[0; / print(42) )"sv; @@ -111,10 +108,10 @@ namespace CppUtils::UnitTest::Language::VirtualMachine if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("Call function: (std::size_t, std::size_t) -> std::size_t", [&] { + suite.test("Call function: (std::size_t, std::size_t) -> std::size_t") = [&] { constexpr auto source = u8R"( [ [30 [12 ()[0; ]] ()[1; [0^] / print(30 + 12); )"sv; @@ -128,11 +125,11 @@ namespace CppUtils::UnitTest::Language::VirtualMachine auto result = VM::execute(source, VM::Functions{add, print}, VM::makeReferences(output)); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - suite.expectEqual(output, 42uz); - }); + expect(result); + expect(output) == 42uz; + }; - suite.addTest("Labels", [&] { + suite.test("Labels") = [&] { constexpr auto source = u8R"( @ @@ -151,10 +148,10 @@ namespace CppUtils::UnitTest::Language::VirtualMachine auto result = VM::execute(source, VM::Functions{add, print}); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("Return value", [&] { + suite.test("Return value") = [&] { constexpr auto source = u8R"( @ @@ -184,110 +181,110 @@ namespace CppUtils::UnitTest::Language::VirtualMachine auto result = VM::execute(source, VM::Functions{add, print}, VM::makeReferences(output)); if (not result) Logger::print<"error">("{}", result.error()); - suite.expect(result); - suite.expectEqual(output, 42uz); - }); + expect(result); + expect(output) == 42uz; + }; /* - suite.addTest("equal", [&] { + suite.test("equal") = [&] { { constexpr auto source = u8"(:0, 2:42, 0, 2:42, 0="sv; constexpr auto result = VM::execute(source); - suite.expect(result); + expect(result); } { constexpr auto source = u8"(:0, 2:42, 0, 2:21, 0="sv; constexpr auto result = VM::execute(source); - suite.expect(!result); + expect(!result); } - }); + }; - suite.addTest("and", [&] { + suite.test("and") = [&] { { constexpr auto source = u8"(:1, 0, 0:1, 0&"sv; constexpr auto result = VM::execute(source); - suite.expect(result); + expect(result); } { constexpr auto source = u8"(:0, 0, 0:1, 0&"sv; constexpr auto result = VM::execute(source); - suite.expect(!result); + expect(!result); } - }); + }; - suite.addTest("or", [&] { + suite.test("or") = [&] { { constexpr auto source = u8"(:0, 0, 0:1, 0|"sv; constexpr auto result = VM::execute(source); - suite.expect(result); + expect(result); } { constexpr auto source = u8"(:0, 0, 0:0, 0|"sv; constexpr auto result = VM::execute(source); - suite.expect(!result); + expect(!result); } - }); + }; - suite.addTest("xor", [&] { + suite.test("xor") = [&] { { constexpr auto source = u8"(:0, 0, 0:1, 0^"sv; constexpr auto result = VM::execute(source); - suite.expect(result); + expect(result); } { constexpr auto source = u8"(:1, 0, 0:1, 0^"sv; constexpr auto result = VM::execute(source); - suite.expect(!result); + expect(!result); } - }); + }; - suite.addTest("external prvalue", [&] { + suite.test("external prvalue") = [&] { constexpr auto source = u8"(:0, 1;"sv; constexpr auto result = VM::execute(source, 42); - suite.expectEqual(result, 42); - }); + expect(result) == 42; + }; - suite.addTest("external lvalue", [&] { + suite.test("external lvalue") = [&] { constexpr auto source = u8"(:0, 1;"sv; constexpr auto number = 42; constexpr auto result = VM::execute(source, number); - suite.expectEqual(result, number); - }); + expect(result) == number; + }; - suite.addTest("external pointer", [&] { + suite.test("external pointer") = [&] { constexpr auto source = u8"(:0, 1;R"sv; constexpr auto number = 42; auto result = VM::execute(source, std::addressof(number)); - suite.expectEqual(result, number); - }); + expect(result) == number; + }; - suite.addTest("external lambda", [&] { + suite.test("external lambda") = [&] { constexpr auto source = u8"(:0, 1;"sv; static constexpr auto function = []() -> int { return 42; }; constexpr auto result = VM::execute(source, +function); - suite.expectEqual(result, function()); - }); + expect(result) == function(); + }; - suite.addTest("external function", [&] { + suite.test("external function") = [&] { constexpr auto source = u8"(0, 1;, 2;)"sv; constexpr auto input = u8"Hello World!"sv; auto result = VM::execute(source, input, std::ranges::size); - suite.expectEqual(result, std::ranges::size(input)); - }); + expect(result) == std::ranges::size(input); + }; - suite.addTest("external member function", [&] { + suite.test("external member function") = [&] { constexpr auto source = u8"(0, 1;, 2;)"sv; constexpr auto input = u8"Hello World!"sv; auto result = VM::execute(source, std::addressof(input), &std::u8string_view::size); - suite.expectEqual(result, std::ranges::size(input)); - }); + expect(result) == std::ranges::size(input); + }; - suite.addTest("conditional (false, direct) jump (direct, relative)", [&] { + suite.test("conditional (false, direct) jump (direct, relative)") = [&] { constexpr auto source = u8"(: (0, 0, 4, 0? 42X 21"sv; constexpr auto result = VM::execute(source); - suite.expectEqual(result, 42); - }); + expect(result) == 42; + }; - suite.addTest("copy", [&] { + suite.test("copy") = [&] { constexpr auto source = u8R"( (0 (21, 0 @@ -295,28 +292,28 @@ namespace CppUtils::UnitTest::Language::VirtualMachine + )"sv; constexpr auto result = VM::execute(source); - suite.expectEqual(result, 42uz); - }); + expect(result) == 42uz; + }; - suite.addTest("cast", [&] { + suite.test("cast") = [&] { constexpr auto source = u8"(:, 1:42, 0, 2, 1, 2C)"sv; constexpr auto result = VM::execute(source); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("swap 1", [&] { + suite.test("swap 1") = [&] { constexpr auto source = u8"(0:42, 1:0S"sv; auto result = VM::execute(source); - suite.expectEqual(result, 42uz); - }); + expect(result) == 42uz; + }; - suite.addTest("swap 2", [&] { + suite.test("swap 2") = [&] { constexpr auto source = u8"(1:0, 0:42S)"sv; auto result = VM::execute(source); - suite.expectEqual(result, 42uz); - }); + expect(result) == 42uz; + }; - suite.addTest("test loops", [&] { + suite.test("test loops") = [&] { constexpr auto source = u8R"( (1; get nb (0, 1, 0+ plus one @@ -339,18 +336,18 @@ namespace CppUtils::UnitTest::Language::VirtualMachine }; auto result = VM::execute(source, nb, input, +print); - suite.expectEqual(result, 0uz); - }); + expect(result) == 0uz; + }; - suite.addTest("char16_t", [&] { + suite.test("char16_t") = [&] { constexpr auto source = uR"( (: (1:\¤, 0, 1:\§, 0= )"sv; auto executionResult = VM::execute(source); - suite.expect(u"¤"sv != u"§"sv); - suite.expectEqual(executionResult, false); - }); + expect(u"¤"sv) != u"§"; + expect(executionResult) == false; + }; */ - }}; + }; } diff --git a/tests/Language/XML/FlowRunner.mpp b/tests/Language/XML/FlowRunner.mpp index 612ce8ad..745afde2 100644 --- a/tests/Language/XML/FlowRunner.mpp +++ b/tests/Language/XML/FlowRunner.mpp @@ -45,11 +45,9 @@ namespace CppUtils::UnitTest::Language::Xml::FlowRunner using FunctionMapping = CppUtils::Type::Mapping<>; }; - auto _ = TestSuite{"Language/Xml/FlowRunner", - {"Language/Xml/Pipeline"}, - [](TestSuite& suite) { - suite.addTest("Discover and execute flows from directory", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& temporaryDirectory) -> void { + auto _ = testSuite("Language/Xml/FlowRunner").dependsOn("Language/Xml/Pipeline") = [](TestSuite& suite) { + suite.test("Discover and execute flows from directory") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& temporaryDirectory) -> void { std::filesystem::create_directories(temporaryDirectory / "flows"); std::filesystem::create_directories(temporaryDirectory / "input"); std::filesystem::create_directories(temporaryDirectory / "output"); @@ -89,43 +87,43 @@ namespace CppUtils::UnitTest::Language::Xml::FlowRunner runner.registerMapping("alpha"); runner.registerMapping("beta"); - suite.expectEqual(std::ranges::size(runner.flows().value()), 0uz); + expect(std::ranges::size(runner.flows().value())) == 0uz; runner.loadFlows(); - suite.expectEqual(std::ranges::size(runner.flows().value()), 2uz); + expect(std::ranges::size(runner.flows().value())) == 2uz; const auto configs = runner.discoverFlowConfigs(); - suite.expectEqual(std::ranges::size(configs), 2uz); + expect(std::ranges::size(configs)) == 2uz; const auto alphaFlowByName = runner.flow("FlowAlpha"); const auto betaFlowByName = runner.flow("FlowBeta"); - suite.expect(alphaFlowByName != nullptr); - suite.expect(betaFlowByName != nullptr); - suite.expect(runner.flow("NonExistent") == nullptr); + expect(alphaFlowByName) != nullptr; + expect(betaFlowByName) != nullptr; + expect(runner.flow("NonExistent")) == nullptr; const auto alphaFlowByPath = runner.flow(temporaryDirectory / "flows/alpha.xml"); const auto betaFlowByPath = runner.flow(temporaryDirectory / "flows/beta.xml"); - suite.expectEqual(alphaFlowByPath, alphaFlowByName); - suite.expectEqual(betaFlowByPath, betaFlowByName); - suite.expect(runner.flow(temporaryDirectory / "flows/missing.xml") == nullptr); + expect(alphaFlowByPath) == alphaFlowByName; + expect(betaFlowByPath) == betaFlowByName; + expect(runner.flow(temporaryDirectory / "flows/missing.xml")) == nullptr; const auto stats = runner.execute(); - suite.expectEqual(stats.processedFilesCount, 2uz); - suite.expect(stats.elapsedDuration >= 0ns); - suite.expect(std::filesystem::exists(temporaryDirectory / "output/out1.csv")); - suite.expect(std::filesystem::exists(temporaryDirectory / "output/out2.csv")); + expect(stats.processedFilesCount) == 2uz; + expect(stats.elapsedDuration) >= 0ns; + expect(std::filesystem::exists(temporaryDirectory / "output/out1.csv")); + expect(std::filesystem::exists(temporaryDirectory / "output/out2.csv")); const auto out1Content = CppUtils::FileSystem::String::read(temporaryDirectory / "output/out1.csv"); - suite.expect(out1Content.contains("rec1;catA;5")); - suite.expect(out1Content.contains("rec2;catB;15")); + expect(out1Content.contains("rec1;catA;5")); + expect(out1Content.contains("rec2;catB;15")); const auto out2Content = CppUtils::FileSystem::String::read(temporaryDirectory / "output/out2.csv"); - suite.expect(out2Content.contains("25;catB;rec4")); - suite.expect(not out2Content.contains("rec3")); + expect(out2Content.contains("25;catB;rec4")); + expect(not out2Content.contains("rec3")); }}; - }); + }; - suite.addTest("Concurrent flow lookups and reloading", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& temporaryDirectory) -> void { + suite.test("Concurrent flow lookups and reloading") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& temporaryDirectory) -> void { std::filesystem::create_directories(temporaryDirectory / "flows"); const auto flowXml = R"( @@ -165,8 +163,8 @@ namespace CppUtils::UnitTest::Language::Xml::FlowRunner running.store(false, std::memory_order_relaxed); readerThreads.clear(); - suite.expect(runner.flow("FlowAlpha") != nullptr); + expect(runner.flow("FlowAlpha")) != nullptr; }}; - }); - }}; + }; + }; } diff --git a/tests/Language/XML/Pipeline.mpp b/tests/Language/XML/Pipeline.mpp index e9240840..dea18c87 100644 --- a/tests/Language/XML/Pipeline.mpp +++ b/tests/Language/XML/Pipeline.mpp @@ -87,21 +87,20 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline using FunctionMapping = FreeFunctionMapping; }; - auto _ = TestSuite{"Language/Xml/Pipeline", - {"Language/Xml/XmlLexer", - "Language/CSV/Mapping", - "Language/CSV/Parsing", - "FileSystem/Directory", - "FileSystem/File", - "FileSystem/FileStaging", - "FileSystem/Watcher", - "Ranges/Parallel"}, - [](TestSuite& suite) { + auto _ = testSuite("Language/Xml/Pipeline") + .dependsOn("Language/Xml/XmlLexer", + "Language/CSV/Mapping", + "Language/CSV/Parsing", + "FileSystem/Directory", + "FileSystem/File", + "FileSystem/FileStaging", + "FileSystem/Watcher", + "Ranges/Parallel") = [](TestSuite& suite) { using namespace std::literals; using namespace std::chrono_literals; using namespace CppUtils::Language::Xml::Literals; - suite.addTest("Empty pipeline", [&] { + suite.test("Empty pipeline") = [&] { const auto xmlTree = R"( @@ -117,10 +116,10 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline | pipeline | std::ranges::to>(); - suite.expectEqual(outputUsers, inputUsers); - }); + expect(outputUsers) == inputUsers; + }; - suite.addTest("Operation value", [&] { + suite.test("Operation value") = [&] { const auto pipeline = CppUtils::Language::Xml::buildPipeline(R"( @@ -130,11 +129,11 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline const auto inputUsers = std::vector{User{"Alice", "alice@example.com", 25, 100, 4.5f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].score, 120); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].score) == 120; + }; - suite.addTest("Operation variable", [&] { + suite.test("Operation variable") = [&] { const auto pipeline = CppUtils::Language::Xml::buildPipeline(R"( @@ -145,11 +144,11 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline const auto inputUsers = std::vector{User{"Alice", "alice@example.com", 25, 100, 4.5f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].score, 135); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].score) == 135; + }; - suite.addTest("Filter value", [&] { + suite.test("Filter value") = [&] { const auto pipeline = CppUtils::Language::Xml::buildPipeline(R"( @@ -162,12 +161,12 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline User{"Charlie", "charlie@example.com", 18, 80, 4.0f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 2uz); - suite.expectEqual(outputUsers[0].name, "Alice"); - suite.expectEqual(outputUsers[1].name, "Charlie"); - }); + expect(std::ranges::size(outputUsers)) == 2uz; + expect(outputUsers[0].name) == "Alice"; + expect(outputUsers[1].name) == "Charlie"; + }; - suite.addTest("Filter variable", [&] { + suite.test("Filter variable") = [&] { const auto pipeline = CppUtils::Language::Xml::buildPipeline(R"( @@ -180,11 +179,11 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline User{"Bob", "bob@example.com", 15, 50, 3.0f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].name, "Alice"); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].name) == "Alice"; + }; - suite.addTest("Filter predicate", [&] { + suite.test("Filter predicate") = [&] { const auto pipeline = CppUtils::Language::Xml::buildPipeline(R"( @@ -196,11 +195,11 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline User{"Bob", "bob@example.com", 15, 50, 3.0f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].name, "Alice"); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].name) == "Alice"; + }; - suite.addTest("Call member function", [&] { + suite.test("Call member function") = [&] { const auto pipeline = CppUtils::Language::Xml::buildPipeline(R"( @@ -210,11 +209,11 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline const auto inputUsers = std::vector{User{"Alice", "alice@example.com", 25, 100, 4.5f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].score, 150); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].score) == 150; + }; - suite.addTest("Call free function", [&] { + suite.test("Call free function") = [&] { const auto pipeline = CppUtils::Language::Xml::buildPipeline(R"( @@ -224,11 +223,11 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline const auto inputUsers = std::vector{User{"Alice", "alice@example.com", 25, 100, 4.5f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].score, 150); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].score) == 150; + }; - suite.addTest("Call lambda", [&] { + suite.test("Call lambda") = [&] { auto pipeline = CppUtils::Language::Xml::Pipeline{}; CppUtils::Language::Xml::registerStandardPipelineTags(pipeline); @@ -246,12 +245,12 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline const auto inputUsers = std::vector{User{"Alice", "alice@example.com", 25, 100, 4.5f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expect(called); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(called); + }; - suite.addTest("Validate and Rejected", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("Validate and Rejected") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto rejectedPath = directory / "rejected.csv"; const auto xmlSource = std::format(R"( @@ -268,15 +267,15 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline User{"Bob", "bob@example.com", 15, 50, 3.0f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].name, "Alice"); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].name) == "Alice"; const auto rejectedContent = CppUtils::FileSystem::String::read(rejectedPath); - suite.expect(rejectedContent.contains("Too young")); + expect(rejectedContent.contains("Too young")); }}; - }); + }; - suite.addTest("Rejected handler callback", [&] { + suite.test("Rejected handler callback") = [&] { auto pipeline = CppUtils::Language::Xml::Pipeline{}; CppUtils::Language::Xml::registerStandardPipelineTags(pipeline); @@ -295,13 +294,13 @@ namespace CppUtils::UnitTest::Language::Xml::Pipeline const auto inputUsers = std::vector{User{"Bob", "bob@example.com", 15, 50, 3.0f}}; const auto outputUsers = inputUsers | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 0uz); - suite.expectEqual(std::ranges::size(rejectedErrors), 1uz); - suite.expectEqual(rejectedErrors[0], "Too young"); - }); + expect(std::ranges::size(outputUsers)) == 0uz; + expect(std::ranges::size(rejectedErrors)) == 1uz; + expect(rejectedErrors[0]) == "Too young"; + }; - suite.addTest("executeFlow", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("executeFlow") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto inputPath = directory / "input_users.csv"; const auto outputPath = directory / "output_users.csv"; const auto rejectedPath = directory / "rejected_users.csv"; @@ -336,9 +335,9 @@ Charlie;charlie@example.com;30;80;4.8)"sv); | std::views::filter([](auto line) { return not std::ranges::empty(line); }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputLines), 2uz); - suite.expectEqual(outputLines[0], "Alice;alice@example.com;25;115;4.5"); - suite.expectEqual(outputLines[1], "Charlie;charlie@example.com;30;95;4.8"); + expect(std::ranges::size(outputLines)) == 2uz; + expect(outputLines[0]) == "Alice;alice@example.com;25;115;4.5"; + expect(outputLines[1]) == "Charlie;charlie@example.com;30;95;4.8"; const auto rejectedContent = CppUtils::FileSystem::String::read(rejectedPath); const auto rejectedLines = rejectedContent @@ -347,14 +346,14 @@ Charlie;charlie@example.com;30;80;4.8)"sv); | std::views::filter([](auto line) { return not std::ranges::empty(line); }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(rejectedLines), 2uz); - suite.expect(rejectedContent.contains("Invalid;bad_line;NaN;0;0.0")); - suite.expect(rejectedContent.contains("Underage user")); + expect(std::ranges::size(rejectedLines)) == 2uz; + expect(rejectedContent.contains("Invalid;bad_line;NaN;0;0.0")); + expect(rejectedContent.contains("Underage user")); }}; - }); + }; - suite.addTest("executeFlow parallelChunk", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("executeFlow parallelChunk") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto inputPath = directory / "input_users.csv"; const auto outputPath = directory / "output_users.csv"; const auto rejectedPath = directory / "rejected_users.csv"; @@ -392,20 +391,20 @@ Frank;frank@example.com;35;70;3.5)"sv); | std::views::filter([](auto line) { return not std::ranges::empty(line); }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputLines), 4uz); - suite.expectEqual(outputLines[0], "Alice;alice@example.com;25;110;4.5"); - suite.expectEqual(outputLines[1], "Charlie;charlie@example.com;30;90;4.8"); - suite.expectEqual(outputLines[2], "David;david@example.com;22;100;4"); - suite.expectEqual(outputLines[3], "Frank;frank@example.com;35;80;3.5"); + expect(std::ranges::size(outputLines)) == 4uz; + expect(outputLines[0]) == "Alice;alice@example.com;25;110;4.5"; + expect(outputLines[1]) == "Charlie;charlie@example.com;30;90;4.8"; + expect(outputLines[2]) == "David;david@example.com;22;100;4"; + expect(outputLines[3]) == "Frank;frank@example.com;35;80;3.5"; const auto rejectedContent = CppUtils::FileSystem::String::read(rejectedPath); - suite.expect(rejectedContent.contains("Invalid;bad_line;NaN;0;0.0")); - suite.expect(rejectedContent.contains("Underage user")); + expect(rejectedContent.contains("Invalid;bad_line;NaN;0;0.0")); + expect(rejectedContent.contains("Underage user")); }}; - }); + }; - suite.addTest("executeFlow FileStaging", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("executeFlow FileStaging") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto inputDir = directory / "input"; const auto stagingDir = directory / "staging"; const auto archiveDir = directory / "archive"; @@ -442,22 +441,22 @@ Bob;bob@example.com;30;80;4.8)"sv); auto threadPool = CppUtils::Thread::ThreadPool{2}; CppUtils::Language::Xml::executeFlow(xmlSource, threadPool); - suite.expect(not std::filesystem::exists(inputFile)); - suite.expect(std::filesystem::exists(outputFile)); + expect(not std::filesystem::exists(inputFile)); + expect(std::filesystem::exists(outputFile)); const auto archiveFiles = std::ranges::count_if( std::filesystem::directory_iterator{archiveDir}, [](const auto& entry) { return entry.is_regular_file(); }); - suite.expectEqual(archiveFiles, 1); + expect(archiveFiles) == 1; const auto outputContent = CppUtils::FileSystem::String::read(outputFile); - suite.expect(outputContent.contains("Alice;alice@example.com;25;105;4.5")); - suite.expect(outputContent.contains("Bob;bob@example.com;30;85;4.8")); + expect(outputContent.contains("Alice;alice@example.com;25;105;4.5")); + expect(outputContent.contains("Bob;bob@example.com;30;85;4.8")); }}; - }); + }; - suite.addTest("Flow Watcher", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("Flow Watcher") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto inputDir = directory / "input"; const auto outputDir = directory / "output"; @@ -489,14 +488,14 @@ Bob;bob@example.com;30;80;4.8)"sv); flow.stopWatching(); - suite.expect(std::filesystem::exists(expectedOutputFile)); + expect(std::filesystem::exists(expectedOutputFile)); const auto content = CppUtils::FileSystem::String::read(expectedOutputFile); - suite.expect(content.contains("Alice;alice@example.com;25;120;4.5")); + expect(content.contains("Alice;alice@example.com;25;120;4.5")); }}; - }); + }; - suite.addTest("_csv literal", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("_csv literal") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto rejectedPath = directory / "rejected.csv"; const auto xmlSource = std::format(R"( @@ -521,16 +520,16 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 2uz); - suite.expectEqual(outputUsers[0], User{"Alice", "alice@example.com", 25, 110, 4.5f}); - suite.expectEqual(outputUsers[1], User{"Charlie", "charlie@example.com", 30, 90, 4.8f}); + expect(std::ranges::size(outputUsers)) == 2uz; + expect(outputUsers[0]) == User{"Alice", "alice@example.com", 25, 110, 4.5f}; + expect(outputUsers[1]) == User{"Charlie", "charlie@example.com", 30, 90, 4.8f}; const auto rejectedContent = CppUtils::FileSystem::String::read(rejectedPath); - suite.expect(rejectedContent.contains("Invalid;corrupted_age;NaN;0;0.0")); + expect(rejectedContent.contains("Invalid;corrupted_age;NaN;0;0.0")); }}; - }); + }; - suite.addTest(" column tag", [&] { + suite.test(" column tag") = [&] { const auto xmlSource = R"( @@ -547,13 +546,13 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 2uz); - suite.expectEqual(outputUsers[0].name, std::string{"ALICE"}); - suite.expectEqual(outputUsers[1].name, std::string{"BOB"}); - }); + expect(std::ranges::size(outputUsers)) == 2uz; + expect(outputUsers[0].name) == std::string{"ALICE"}; + expect(outputUsers[1].name) == std::string{"BOB"}; + }; - suite.addTest("Custom tag", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("Custom tag") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto rejectedPath = directory / "rejected.csv"; const auto xmlSource = std::format(R"( @@ -589,15 +588,15 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0], User{"Dr. Alice", "alice@example.com", 25, 100, 4.5f}); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0]) == User{"Dr. Alice", "alice@example.com", 25, 100, 4.5f}; const auto rejectedContent = CppUtils::FileSystem::String::read(rejectedPath); - suite.expect(rejectedContent.contains("Invalid;corrupt@example.com;NaN;0;0.0")); + expect(rejectedContent.contains("Invalid;corrupt@example.com;NaN;0;0.0")); }}; - }); + }; - suite.addTest(" condition", [&] { + suite.test(" condition") = [&] { const auto xmlSource = R"( @@ -618,12 +617,12 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 2uz); - suite.expectEqual(outputUsers[0], User{"Alice", "alice@example.com", 25, 120, 4.5f}); - suite.expectEqual(outputUsers[1], User{"Bob", "bob@example.com", 15, 50, 3.0f}); - }); + expect(std::ranges::size(outputUsers)) == 2uz; + expect(outputUsers[0]) == User{"Alice", "alice@example.com", 25, 120, 4.5f}; + expect(outputUsers[1]) == User{"Bob", "bob@example.com", 15, 50, 3.0f}; + }; - suite.addTest(" with and ", [&] { + suite.test(" with and ") = [&] { const auto xmlSource = R"( @@ -653,14 +652,14 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 4uz); - suite.expectEqual(outputUsers[0], User{"Alice", "alice@example.com", 25, 150, 4.5f}); - suite.expectEqual(outputUsers[1], User{"Bob", "bob@example.com", 30, 40, 3.0f}); - suite.expectEqual(outputUsers[2], User{"Charlie", "charlie@example.com", 20, 80, 4.0f}); - suite.expectEqual(outputUsers[3], User{"David", "david@example.com", 15, 100, 3.5f}); - }); + expect(std::ranges::size(outputUsers)) == 4uz; + expect(outputUsers[0]) == User{"Alice", "alice@example.com", 25, 150, 4.5f}; + expect(outputUsers[1]) == User{"Bob", "bob@example.com", 30, 40, 3.0f}; + expect(outputUsers[2]) == User{"Charlie", "charlie@example.com", 20, 80, 4.0f}; + expect(outputUsers[3]) == User{"David", "david@example.com", 15, 100, 3.5f}; + }; - suite.addTest("", [&] { + suite.test("") = [&] { const auto xmlSource = R"( @@ -681,12 +680,12 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0], User{"Alice", "alice@example.com", 25, 130, 4.5f}); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0]) == User{"Alice", "alice@example.com", 25, 130, 4.5f}; + }; - suite.addTest("", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto rulesPath = directory / "rules.xml"; CppUtils::FileSystem::String::write(rulesPath, R"( @@ -715,13 +714,13 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 2uz); - suite.expectEqual(outputUsers[0], User{"Alice", "alice@example.com", 25, 115, 4.5f}); - suite.expectEqual(outputUsers[1], User{"Bob", "bob@example.com", 16, 50, 3.0f}); + expect(std::ranges::size(outputUsers)) == 2uz; + expect(outputUsers[0]) == User{"Alice", "alice@example.com", 25, 115, 4.5f}; + expect(outputUsers[1]) == User{"Bob", "bob@example.com", 16, 50, 3.0f}; }}; - }); + }; - suite.addTest("", [&] { + suite.test("") = [&] { const auto xmlSource = R"( @@ -739,11 +738,11 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].name, "Alice"); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].name) == "Alice"; + }; - suite.addTest(" with custom loggerName", [&] { + suite.test(" with custom loggerName") = [&] { auto loggedMessages = std::vector{}; const auto subscriptionIdentifier = CppUtils::Logger<"CustomPipelineLogger">::subscribe<"debug">([&loggedMessages](const std::string& message) { loggedMessages.push_back(message); @@ -769,14 +768,14 @@ Bob;bob@example.com;30;80;4.8)"sv); CppUtils::Logger<"CustomPipelineLogger">::waitUntilFinished(); CppUtils::Logger<"CustomPipelineLogger">::unsubscribe(subscriptionIdentifier); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].name, "Alice"); - suite.expectEqual(std::ranges::size(loggedMessages), 1uz); - suite.expectEqual(loggedMessages[0], "Processing user"); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].name) == "Alice"; + expect(std::ranges::size(loggedMessages)) == 1uz; + expect(loggedMessages[0]) == "Processing user"; + }; - suite.addTest("Flow with ThreadPool", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) { + suite.test("Flow with ThreadPool") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) { const auto inputPath = directory / "input.csv"; const auto outputPath = directory / "output.csv"; @@ -798,10 +797,10 @@ Bob;bob@example.com;30;80;4.8)"sv); auto flowWithOutput = CppUtils::Language::Xml::buildFlow(xmlSourceWithOutput, threadPool); flowWithOutput.execute(); - suite.expect(std::filesystem::exists(outputPath)); + expect(std::filesystem::exists(outputPath)); const auto outputContent = CppUtils::FileSystem::String::read(outputPath); - suite.expect(outputContent.contains("Alice")); - suite.expect(outputContent.contains("110")); + expect(outputContent.contains("Alice")); + expect(outputContent.contains("110")); auto loggedMessages = std::vector{}; const auto subscriptionIdentifier = CppUtils::Logger<"FlowWithoutOutputTest">::subscribe<"info">([&loggedMessages](const std::string& message) { @@ -825,24 +824,24 @@ Bob;bob@example.com;30;80;4.8)"sv); CppUtils::Logger<"FlowWithoutOutputTest">::waitUntilFinished(); CppUtils::Logger<"FlowWithoutOutputTest">::unsubscribe(subscriptionIdentifier); - suite.expectEqual(std::ranges::size(loggedMessages), 1uz); - suite.expect(loggedMessages[0].contains("110")); + expect(std::ranges::size(loggedMessages)) == 1uz; + expect(loggedMessages[0].contains("110")); }}; - }); + }; - suite.addTest("Unknown pipeline tag throws", [&] { + suite.test("Unknown pipeline tag throws") = [&] { const auto xmlSource = R"( )"sv; - suite.expectThrow([&] { + expect([&] { auto _ = CppUtils::Language::Xml::buildPipeline(xmlSource); - }); - }); + }) == throws; + }; - suite.addTest("Unknown Flow tag throws", [&] { + suite.test("Unknown Flow tag throws") = [&] { const auto xmlSource = R"( @@ -850,12 +849,12 @@ Bob;bob@example.com;30;80;4.8)"sv); )"sv; auto threadPool = CppUtils::Thread::ThreadPool{1}; - suite.expectThrow([&] { + expect([&] { auto _ = CppUtils::Language::Xml::buildFlow(xmlSource, threadPool); - }); - }); + }) == throws; + }; - suite.addTest(" with typed and auto-detected variables", [&] { + suite.test(" with typed and auto-detected variables") = [&] { const auto xmlSource = R"( @@ -876,12 +875,12 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].score, 125); - suite.expectEqual(outputUsers[0].name, "Alice_Updated"); - }); + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].score) == 125; + expect(outputUsers[0].name) == "Alice_Updated"; + }; - suite.addTest(" with composite condition", [&] { + suite.test(" with composite condition") = [&] { const auto xmlSource = R"( @@ -906,8 +905,8 @@ Bob;bob@example.com;30;80;4.8)"sv); | pipeline | std::ranges::to>(); - suite.expectEqual(std::ranges::size(outputUsers), 1uz); - suite.expectEqual(outputUsers[0].name, "Alice"); - }); - }}; + expect(std::ranges::size(outputUsers)) == 1uz; + expect(outputUsers[0].name) == "Alice"; + }; + }; } diff --git a/tests/Language/XML/Utility.mpp b/tests/Language/XML/Utility.mpp new file mode 100644 index 00000000..41a8d1d8 --- /dev/null +++ b/tests/Language/XML/Utility.mpp @@ -0,0 +1,15 @@ +export module CppUtils.UnitTests.Language.Xml.Utility; + +import std; +import CppUtils; + +namespace CppUtils::UnitTest::Language::Xml::Utility +{ + auto _ = testSuite("Language/Xml/Utility") = [](TestSuite& suite) { + suite.test("escapeXml handles reserved characters") = [&] { + expect(CppUtils::Language::Xml::escapeXml(" & \"world\" 'test'")) == "<hello> & "world" 'test'"; + expect(CppUtils::Language::Xml::escapeXml("clean string")) == "clean string"; + expect(CppUtils::Language::Xml::escapeXml("")) == ""; + }; + }; +} diff --git a/tests/Language/XML/XmlLexer.mpp b/tests/Language/XML/XmlLexer.mpp index eb5ca0fd..61b400e0 100644 --- a/tests/Language/XML/XmlLexer.mpp +++ b/tests/Language/XML/XmlLexer.mpp @@ -5,34 +5,34 @@ import CppUtils; namespace CppUtils::UnitTest::Language::Xml::XmlLexer { - auto _ = TestSuite{"Language/Xml/XmlLexer", {}, [](TestSuite& suite) { + auto _ = testSuite("Language/Xml/XmlLexer") = [](TestSuite& suite) { using namespace std::literals; using namespace CppUtils::String::Literals; using namespace CppUtils::Language::Xml::Literals; - suite.addTest("Empty tag", [&] { + suite.test("Empty tag") = [&] { const auto xmlTree = R"(
)"_xml; CppUtils::Log::TreeNodeLogger::log(xmlTree); - suite.expect(xmlTree.exists("div"_token)); + expect(xmlTree.exists("div"_token)); const auto& divNode = xmlTree["div"_token]; - suite.expect(std::ranges::empty(divNode.nodes)); - }); + expect(std::ranges::empty(divNode.nodes)); + }; - suite.addTest("Tag", [&] { + suite.test("Tag") = [&] { const auto xmlTree = R"(

Title

)"_xml; CppUtils::Log::TreeNodeLogger::log(xmlTree); - suite.expect(xmlTree.exists("h1"_token)); + expect(xmlTree.exists("h1"_token)); const auto& h1 = xmlTree["h1"_token]; - suite.expectEqual(std::get(h1.nodes.at(0).value), "Title"s); - }); + expect(std::get(h1.nodes.at(0).value)) == "Title"; + }; - suite.addTest("Nested tags", [&] { + suite.test("Nested tags") = [&] { const auto xmlTree = R"(
  • First
  • @@ -42,46 +42,46 @@ namespace CppUtils::UnitTest::Language::Xml::XmlLexer )"_xml; CppUtils::Log::TreeNodeLogger::log(xmlTree); - suite.expect(xmlTree.exists("ul"_token)); + expect(xmlTree.exists("ul"_token)); const auto& ul = xmlTree["ul"_token]; - suite.expectEqual(std::ranges::size(ul.nodes), 3uz); + expect(std::ranges::size(ul.nodes)) == 3uz; for (const auto& li : ul.nodes) - suite.expectEqual(std::get(li.value), "li"_token); - suite.expectEqual(std::ranges::size(ul.nodes.at(0).nodes), 1uz); - suite.expectEqual(std::get(ul.nodes.at(0).nodes.at(0).value), "First"s); - suite.expectEqual(std::get(ul.nodes.at(1).nodes.at(0).value), "Second"s); - suite.expectEqual(std::get(ul.nodes.at(2).nodes.at(0).value), "Third"s); - }); + expect(std::get(li.value)) == "li"_token; + expect(std::ranges::size(ul.nodes.at(0).nodes)) == 1uz; + expect(std::get(ul.nodes.at(0).nodes.at(0).value)) == "First"; + expect(std::get(ul.nodes.at(1).nodes.at(0).value)) == "Second"; + expect(std::get(ul.nodes.at(2).nodes.at(0).value)) == "Third"; + }; - suite.addTest("Attributes", [&] { + suite.test("Attributes") = [&] { const auto xmlTree = R"(

    Title

    )"_xml; CppUtils::Log::TreeNodeLogger::log(xmlTree); - suite.expect(xmlTree.exists("h1"_token)); + expect(xmlTree.exists("h1"_token)); const auto& h1 = xmlTree["h1"_token]; - suite.expect(h1.exists("attributes"_token)); + expect(h1.exists("attributes"_token)); const auto& attrs = h1["attributes"_token]; - suite.expect(attrs.exists("color"_token)); - suite.expectEqual(std::get(attrs["color"_token].nodes.at(0).value), "red"s); - suite.expectEqual(std::get(h1.nodes.at(1).value), "Title"s); - }); + expect(attrs.exists("color"_token)); + expect(std::get(attrs["color"_token].nodes.at(0).value)) == "red"; + expect(std::get(h1.nodes.at(1).value)) == "Title"; + }; - suite.addTest("Self-closing tag", [&] { + suite.test("Self-closing tag") = [&] { const auto xmlTree = R"( Profile )"_xml; CppUtils::Log::TreeNodeLogger::log(xmlTree); - suite.expect(xmlTree.exists("img"_token)); + expect(xmlTree.exists("img"_token)); const auto& imageNode = xmlTree["img"_token]; - suite.expect(imageNode.exists("attributes"_token)); + expect(imageNode.exists("attributes"_token)); const auto& attributesNode = imageNode["attributes"_token]; - suite.expect(attributesNode.exists("src"_token)); - suite.expectEqual(std::get(attributesNode["src"_token].nodes.at(0).value), "avatar.png"s); - suite.expect(attributesNode.exists("alt"_token)); - suite.expectEqual(std::get(attributesNode["alt"_token].nodes.at(0).value), "Profile"s); - }); - }}; + expect(attributesNode.exists("src"_token)); + expect(std::get(attributesNode["src"_token].nodes.at(0).value)) == "avatar.png"; + expect(attributesNode.exists("alt"_token)); + expect(std::get(attributesNode["alt"_token].nodes.at(0).value)) == "Profile"; + }; + }; } diff --git a/tests/Log/ChronoLogger.mpp b/tests/Log/ChronoLogger.mpp index 4def3c1c..fc438843 100644 --- a/tests/Log/ChronoLogger.mpp +++ b/tests/Log/ChronoLogger.mpp @@ -5,21 +5,21 @@ import CppUtils; namespace CppUtils::UnitTest::Log::ChronoLogger { - auto _ = TestSuite{"Log", {"Logger", "Chrono"}, [](TestSuite& suite) { + auto _ = testSuite("Log").dependsOn("Logger", "Chrono") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("ChronoLogger", [&] { + suite.test("ChronoLogger") = [&] { auto chronoLogger = CppUtils::Log::ChronoLogger{[](std::string_view duration) { Logger::emit<"info">("ChronoLogger duration: {}", duration); }}; chronoLogger.stop(); - }); + }; - suite.addTest("ChronoLogger with custom logger", [&] { + suite.test("ChronoLogger with custom logger") = [&] { auto chronoLogger = CppUtils::Log::ChronoLogger{[](std::string_view duration) { Logger::template emit<"info">("TestWithSuiteLogger duration: {}", duration); }}; chronoLogger.stop(); - }); - }}; + }; + }; } diff --git a/tests/Log/FileSink.mpp b/tests/Log/FileSink.mpp index 54310c96..4b63e88a 100644 --- a/tests/Log/FileSink.mpp +++ b/tests/Log/FileSink.mpp @@ -5,20 +5,20 @@ import CppUtils; namespace CppUtils::UnitTest::Log::FileSink { - auto _ = TestSuite{"Log/FileSink", {"Logger", "FileSystem/Directory"}, [](TestSuite& suite) { + auto _ = testSuite("Log/FileSink").dependsOn("Logger", "FileSystem/Directory") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("In file", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("In file") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { const auto logPath = directory / "test.log"; CppUtils::Log::addFileSink<"FileLogger">({.filePath = logPath}); CppUtils::Logger<"FileLogger">::print("Message"); CppUtils::Logger<"FileLogger">::waitUntilFinished(); - suite.expect(std::filesystem::exists(logPath)); - suite.expectEqual(CppUtils::FileSystem::String::read(logPath), "Message"s); + expect(std::filesystem::exists(logPath)); + expect(CppUtils::FileSystem::String::read(logPath)) == "Message"; }}; - }); - }}; -} \ No newline at end of file + }; + }; +} diff --git a/tests/Log/LogRotate.mpp b/tests/Log/LogRotate.mpp index ee18a322..b70027b0 100644 --- a/tests/Log/LogRotate.mpp +++ b/tests/Log/LogRotate.mpp @@ -5,11 +5,11 @@ import CppUtils; namespace CppUtils::UnitTest::Log::LogRotate { - auto _ = TestSuite{"LogRotate", {"Logger", "FileSystem/Directory"}, [](TestSuite& suite) { + auto _ = testSuite("LogRotate").dependsOn("Logger", "FileSystem/Directory") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("Log Rotation", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Log Rotation") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { const auto logPath = directory / "file.log"; CppUtils::Log::addFileSink<"TestRotate">(LogRotateOptions{ .filePath = logPath, @@ -19,21 +19,21 @@ namespace CppUtils::UnitTest::Log::LogRotate CppUtils::Logger<"TestRotate">::print("First message"); CppUtils::Logger<"TestRotate">::waitUntilFinished(); - suite.expect(std::filesystem::exists(logPath)); + expect(std::filesystem::exists(logPath)); CppUtils::Logger<"TestRotate">::print("Second message"); // Triggers rotation CppUtils::Logger<"TestRotate">::waitUntilFinished(); const auto secondFilePath = directory / "file1.log"; - suite.expect(std::filesystem::exists(secondFilePath)); - suite.expect(std::filesystem::exists(logPath)); - suite.expectEqual(CppUtils::FileSystem::String::read(secondFilePath), "First message"s); - suite.expectEqual(CppUtils::FileSystem::String::read(logPath), "Second message"s); + expect(std::filesystem::exists(secondFilePath)); + expect(std::filesystem::exists(logPath)); + expect(CppUtils::FileSystem::String::read(secondFilePath)) == "First message"; + expect(CppUtils::FileSystem::String::read(logPath)) == "Second message"; }}; - }); + }; - suite.addTest("Full Log Rotation", [&] { - CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void { + suite.test("Full Log Rotation") = [&] { + CppUtils::FileSystem::TemporaryDirectory{[](const auto& directory) -> void { const auto logPath = directory / "file.log"; CppUtils::Log::addFileSink<"TestFullRotate">(LogRotateOptions{ .filePath = logPath, @@ -45,29 +45,29 @@ namespace CppUtils::UnitTest::Log::LogRotate CppUtils::Logger<"TestFullRotate">::print("{}", longString); CppUtils::Logger<"TestFullRotate">::waitUntilFinished(); - suite.expect(std::filesystem::exists(logPath)); + expect(std::filesystem::exists(logPath)); CppUtils::Logger<"TestFullRotate">::print("{}", longString); // Triggers rotation CppUtils::Logger<"TestFullRotate">::waitUntilFinished(); - suite.expect(std::filesystem::exists(logPath)); - suite.expect(std::filesystem::exists(directory / "file1.log")); + expect(std::filesystem::exists(logPath)); + expect(std::filesystem::exists(directory / "file1.log")); CppUtils::Logger<"TestFullRotate">::print("{}", longString); // Triggers rotation CppUtils::Logger<"TestFullRotate">::waitUntilFinished(); - suite.expect(std::filesystem::exists(logPath)); - suite.expect(std::filesystem::exists(directory / "file1.log")); - suite.expect(std::filesystem::exists(directory / "file2.log")); + expect(std::filesystem::exists(logPath)); + expect(std::filesystem::exists(directory / "file1.log")); + expect(std::filesystem::exists(directory / "file2.log")); CppUtils::Logger<"TestFullRotate">::print("{}", longString); // Triggers rotation CppUtils::Logger<"TestFullRotate">::waitUntilFinished(); - suite.expect(std::filesystem::exists(logPath)); - suite.expect(std::filesystem::exists(directory / "file1.log")); - suite.expect(std::filesystem::exists(directory / "file2.log")); - suite.expect(not std::filesystem::exists(directory / "file3.log")); + expect(std::filesystem::exists(logPath)); + expect(std::filesystem::exists(directory / "file1.log")); + expect(std::filesystem::exists(directory / "file2.log")); + expect(not std::filesystem::exists(directory / "file3.log")); }}; - }); - }}; + }; + }; } diff --git a/tests/Log/Logger.mpp b/tests/Log/Logger.mpp index e099a9ea..8d6a5ee2 100644 --- a/tests/Log/Logger.mpp +++ b/tests/Log/Logger.mpp @@ -5,11 +5,11 @@ import CppUtils; namespace CppUtils::UnitTest::Log::Logger { - auto _ = TestSuite{"Logger", {"UnitTest"}, [](TestSuite& suite) { + auto _ = testSuite("Logger").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("In console", [&] { + suite.test("In console") = [&] { Logger::print("Message"); Logger::print<"info">("Information message"); @@ -19,9 +19,9 @@ namespace CppUtils::UnitTest::Log::Logger Logger::print<"detail">("Detail message"); Logger::print<"warning">("Warning message"); Logger::print<"error">("Error message"); - }); + }; - suite.addTest("Subscribe and print", [&] { + suite.test("Subscribe and print") = [&] { using TestLogger = CppUtils::Logger<"TestLogger">; auto loggedMessages = std::vector{}; @@ -32,14 +32,14 @@ namespace CppUtils::UnitTest::Log::Logger TestLogger::print("Hello {}", "world"); TestLogger::waitUntilFinished(); - suite.expectEqual(std::ranges::size(loggedMessages), 1uz); + expect(std::ranges::size(loggedMessages)) == 1uz; if (not std::ranges::empty(loggedMessages)) - suite.expectEqual(loggedMessages.front(), "Hello world"); + expect(loggedMessages.front()) == "Hello world"; - suite.expect(TestLogger::unsubscribe(subscriptionIdentifier)); - }); + expect(TestLogger::unsubscribe(subscriptionIdentifier)); + }; - suite.addTest("Unsubscribe", [&] { + suite.test("Unsubscribe") = [&] { using TestLogger = CppUtils::Logger<"TestLogger">; auto count = 0; @@ -49,18 +49,18 @@ namespace CppUtils::UnitTest::Log::Logger TestLogger::emit<"custom">(); TestLogger::waitUntilFinished(); - suite.expectEqual(count, 1); + expect(count) == 1; - suite.expect(TestLogger::unsubscribe(subscriptionIdentifier)); + expect(TestLogger::unsubscribe(subscriptionIdentifier)); TestLogger::emit<"custom">(); TestLogger::waitUntilFinished(); - suite.expectEqual(count, 1); + expect(count) == 1; - suite.expect(not TestLogger::unsubscribe(subscriptionIdentifier)); - }); + expect(not TestLogger::unsubscribe(subscriptionIdentifier)); + }; - suite.addTest("Clear subscribers", [&] { + suite.test("Clear subscribers") = [&] { using TestLogger = CppUtils::Logger<"TestLogger">; auto countA = 0; auto countB = 0; @@ -71,19 +71,19 @@ namespace CppUtils::UnitTest::Log::Logger TestLogger::emit<"typeA">(); TestLogger::emit<"typeB">(); TestLogger::waitUntilFinished(); - suite.expectEqual(countA, 1); - suite.expectEqual(countB, 1); + expect(countA) == 1; + expect(countB) == 1; TestLogger::clearSubscribers(); TestLogger::emit<"typeA">(); TestLogger::emit<"typeB">(); TestLogger::waitUntilFinished(); - suite.expectEqual(countA, 1); - suite.expectEqual(countB, 1); - }); + expect(countA) == 1; + expect(countB) == 1; + }; - suite.addTest("Unsubscribe by log type", [&] { + suite.test("Unsubscribe by log type") = [&] { using TestLogger = CppUtils::Logger<"TestLogger">; auto countA = 0; auto countB = 0; @@ -94,18 +94,18 @@ namespace CppUtils::UnitTest::Log::Logger TestLogger::emit<"typeA">(); TestLogger::emit<"typeB">(); TestLogger::waitUntilFinished(); - suite.expectEqual(countA, 1); - suite.expectEqual(countB, 1); + expect(countA) == 1; + expect(countB) == 1; - suite.expect(TestLogger::unsubscribe<"typeA">()); + expect(TestLogger::unsubscribe<"typeA">()); TestLogger::emit<"typeA">(); TestLogger::emit<"typeB">(); TestLogger::waitUntilFinished(); - suite.expectEqual(countA, 1); - suite.expectEqual(countB, 2); + expect(countA) == 1; + expect(countB) == 2; TestLogger::clearSubscribers(); - }); - }}; + }; + }; } diff --git a/tests/Math/BigInt.mpp b/tests/Math/BigInt.mpp index f0d682fe..ee3f52f9 100644 --- a/tests/Math/BigInt.mpp +++ b/tests/Math/BigInt.mpp @@ -5,137 +5,137 @@ import CppUtils; namespace CppUtils::UnitTest::Math { - auto _ = TestSuite{"Math/BigInt", [](auto& suite) { + auto _ = testSuite("Math/BigInt") = [](TestSuite& suite) { using namespace CppUtils::Math::Literals; using CppUtils::Math::BigUInt; using CppUtils::Math::BigInt; - suite.addTest("BigUInt Construction & Equality", [&] { + suite.test("BigUInt Construction & Equality") = [&] { const auto firstValue = 12'345_bigUInt; const auto secondValue = 0x30'39_bigUInt; - suite.expect(firstValue == secondValue); + expect(firstValue) == secondValue; const auto bytes = firstValue.toBytes(); - suite.expectEqual(std::ranges::size(bytes), 2uz); - suite.expectEqual(static_cast(bytes[0]), 0x30); - suite.expectEqual(static_cast(bytes[1]), 0x39); - }); + expect(std::ranges::size(bytes)) == 2uz; + expect(bytes[0]) == 0x30; + expect(bytes[1]) == 0x39; + }; - suite.addTest("BigInt Signed Construction & Negation", [&] { + suite.test("BigInt Signed Construction & Negation") = [&] { const auto positiveValue = 12'345_bigInt; const auto negativeValue = -positiveValue; - suite.expect(positiveValue.isNegative() == false); - suite.expect(negativeValue.isNegative() == true); - suite.expect(negativeValue < positiveValue); - suite.expect(positiveValue + negativeValue == 0_bigInt); + not expect(positiveValue.isNegative()); + expect(negativeValue.isNegative()); + expect(negativeValue) < positiveValue; + expect(positiveValue + negativeValue) == 0_bigInt; const auto parsedNegativeValue = "-12345"_bigInt; - suite.expect(parsedNegativeValue == negativeValue); - }); + expect(parsedNegativeValue) == negativeValue; + }; - suite.addTest("BigInt Signed Addition & Subtraction", [&] { + suite.test("BigInt Signed Addition & Subtraction") = [&] { auto value = 10_bigInt; value += 20_bigInt; - suite.expect(value == 30_bigInt); + expect(value) == 30_bigInt; value -= 50_bigInt; - suite.expect(value == -20_bigInt); - suite.expect(value.isNegative()); + expect(value) == -20_bigInt; + expect(value.isNegative()); value += 30_bigInt; - suite.expect(value == 10_bigInt); - suite.expect(value.isNegative() == false); - }); + expect(value) == 10_bigInt; + not expect(value.isNegative()); + }; - suite.addTest("BigInt Signed Multiplication & Division", [&] { + suite.test("BigInt Signed Multiplication & Division") = [&] { const auto firstOperand = -10_bigInt; const auto secondOperand = 20_bigInt; const auto product = firstOperand * secondOperand; - suite.expect(product == -200_bigInt); - suite.expect(product.isNegative()); + expect(product) == -200_bigInt; + expect(product.isNegative()); const auto quotient = product / firstOperand; - suite.expect(quotient == 20_bigInt); - suite.expect(quotient.isNegative() == false); - }); + expect(quotient) == 20_bigInt; + not expect(quotient.isNegative()); + }; - suite.addTest("ModPow", [&] { + suite.test("ModPow") = [&] { const auto base = 2_bigUInt; const auto exponent = 10_bigUInt; const auto modulus = 1'000_bigUInt; const auto result = base.modPow(exponent, modulus); - suite.expectEqual(result, 24_bigUInt); - }); + expect(result) == 24_bigUInt; + }; - suite.addTest("Large ModPow", [&] { + suite.test("Large ModPow") = [&] { const auto base = 123_bigUInt; const auto exponent = 456_bigUInt; const auto modulus = 789_bigUInt; const auto result = base.modPow(exponent, modulus); - suite.expectEqual(result, 699_bigUInt); - }); + expect(result) == 699_bigUInt; + }; - suite.addTest("Decimal Literal (Large)", [&] { + suite.test("Decimal Literal (Large)") = [&] { const auto value = 12'345'678'901'234'567'890_bigUInt; const auto bytes = value.toBytes(); - suite.expectEqual(std::ranges::size(bytes), 8uz); - suite.expectEqual(static_cast(bytes[0]), 0xAB); - suite.expectEqual(static_cast(bytes[7]), 0xD2); - }); + expect(std::ranges::size(bytes)) == 8uz; + expect(bytes[0]) == 0xAB; + expect(bytes[7]) == 0xD2; + }; - suite.addTest("Parsing (decimal)", [&] { + suite.test("Parsing (decimal)") = [&] { const auto validPositive = BigInt::parse("12345"); - suite.expect(validPositive.has_value()); - suite.expect(validPositive.value() == 12345_bigInt); - }); + expect(validPositive.has_value()); + expect(validPositive.value()) == 12345_bigInt; + }; - suite.addTest("Parsing (negative decimal)", [&] { + suite.test("Parsing (negative decimal)") = [&] { const auto validSigned = BigInt::parse("-12345"); - suite.expect(validSigned.has_value()); - suite.expect(validSigned.value() == -12345_bigInt); - }); + expect(validSigned.has_value()); + expect(validSigned.value()) == -12345_bigInt; + }; - suite.addTest("Parsing (invalid unsigned)", [&] { + suite.test("Parsing (invalid unsigned)") = [&] { const auto invalidUnsigned = BigUInt::parse("-10"); - suite.expect(not invalidUnsigned.has_value()); - }); + expect(not invalidUnsigned.has_value()); + }; - suite.addTest("Parsing (hexadecimal)", [&] { + suite.test("Parsing (hexadecimal)") = [&] { const auto validHex = BigInt::parse("0x3039"); - suite.expect(validHex.has_value()); - suite.expect(validHex.value() == 12345_bigInt); - }); + expect(validHex.has_value()); + expect(validHex.value()) == 12345_bigInt; + }; - suite.addTest("Parsing (invalid hexadecimal)", [&] { + suite.test("Parsing (invalid hexadecimal)") = [&] { const auto invalidHex = BigInt::parse("0xGHI"); - suite.expect(not invalidHex.has_value()); - }); + expect(not invalidHex.has_value()); + }; - suite.addTest("divMod", [&] { + suite.test("divMod") = [&] { const auto numerator = 100_bigInt; const auto denominator = 7_bigInt; const auto divResult = numerator.divMod(denominator); - suite.expect(divResult.has_value()); - suite.expectEqual(divResult->quotient, 14_bigInt); - suite.expectEqual(divResult->remainder, 2_bigInt); + expect(divResult.has_value()); + expect(divResult->quotient) == 14_bigInt; + expect(divResult->remainder) == 2_bigInt; const auto divZeroResult = numerator.divMod(0_bigInt); - suite.expect(not divZeroResult.has_value()); - }); + expect(not divZeroResult.has_value()); + }; - suite.addTest("constexpr support", [&] { + suite.test("constexpr support") = [&] { const auto result = []() constexpr { auto leftOperand = 10_bigInt; auto rightOperand = -20_bigInt; return leftOperand + rightOperand == -10_bigInt; }(); - suite.expect(result); - }); - }}; + expect(result); + }; + }; } diff --git a/tests/Math/Endian.mpp b/tests/Math/Endian.mpp index 60fd72c3..2f007432 100644 --- a/tests/Math/Endian.mpp +++ b/tests/Math/Endian.mpp @@ -5,17 +5,17 @@ import CppUtils; namespace CppUtils::UnitTest::Math::Endian { - auto _ = TestSuite{"Endian", {"UnitTest"}, [](TestSuite& suite) { - constexpr auto value = std::uint32_t{0x12'34'56'78u}; + auto _ = testSuite("Endian").dependsOn("UnitTest") = [](TestSuite& suite) { + constexpr auto sourceValue = std::uint32_t{0x12'34'56'78u}; - suite.addTest("toBigEndian", [&] { + suite.test("toBigEndian") = [&] { constexpr auto expectedBig = (std::endian::native == std::endian::big) ? 0x12'34'56'78u : 0x78'56'34'12u; - suite.expectEqual(CppUtils::Math::toBigEndian(value), expectedBig); - }); + expect(CppUtils::Math::toBigEndian(sourceValue)) == expectedBig; + }; - suite.addTest("toLittleEndian", [&] { + suite.test("toLittleEndian") = [&] { constexpr auto expectedLittle = (std::endian::native == std::endian::little) ? 0x12'34'56'78u : 0x78'56'34'12u; - suite.expectEqual(CppUtils::Math::toLittleEndian(value), expectedLittle); - }); - }}; + expect(CppUtils::Math::toLittleEndian(sourceValue)) == expectedLittle; + }; + }; } diff --git a/tests/Math/Random.mpp b/tests/Math/Random.mpp index 7279fc1c..7a225979 100644 --- a/tests/Math/Random.mpp +++ b/tests/Math/Random.mpp @@ -5,15 +5,69 @@ import CppUtils; namespace CppUtils::UnitTest::Math::Random { - auto _ = TestSuite{"Random", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("getRandomNumberInInterval", [&] { + auto _ = testSuite("Random").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test("getRandomNumberInInterval") = [&] { constexpr auto min = 1; constexpr auto max = 10; for (int i = 0; i < 100; ++i) { auto randomNumber = CppUtils::Math::getRandomNumberInInterval(min, max); - suite.expect(randomNumber >= min and randomNumber <= max); + expect(randomNumber >= min and randomNumber <= max); } - }); - }}; + }; + + suite.test("DeterministicRandomEngine") = [&] { + auto engine1 = CppUtils::Math::DeterministicRandomEngine{42ULL}; + auto engine2 = CppUtils::Math::DeterministicRandomEngine{42ULL}; + + expect(engine1.seed()) == 42ULL; + expect(engine2.seed()) == 42ULL; + + for (auto _ : std::views::iota(0uz, 100uz)) + expect(engine1.nextUint64()) == engine2.nextUint64(); + + const auto boolean = engine1.random(); + Execution::doNotOptimize(boolean); + + const auto integer = engine1.random(10, 20); + expect(integer >= 10 and integer <= 20); + + const auto floating = engine1.randomFloat(0.5, 1.5); + expect(floating >= 0.5 and floating <= 1.5); + + const auto randomGenericBool = engine1.random(); + Execution::doNotOptimize(randomGenericBool); + + const auto randomGenericInt = engine1.random(); + Execution::doNotOptimize(randomGenericInt); + + const auto randomGenericDouble = engine1.random(); + expect(randomGenericDouble >= 0.0 and randomGenericDouble <= 1.0); + + const auto randomDeducedRangeInt = engine1.random(10, 20); + expect(randomDeducedRangeInt >= 10 and randomDeducedRangeInt <= 20); + + const auto randomDeducedRangeFloat = engine1.random(0.5, 1.5); + expect(randomDeducedRangeFloat >= 0.5 and randomDeducedRangeFloat <= 1.5); + + const auto sizeWithRange = engine1.randomSize(10uz, 50uz); + expect(sizeWithRange >= 10uz and sizeWithRange <= 50uz); + + const auto sizeWithMaximum = engine1.randomSize(100uz); + expect(sizeWithMaximum) <= 100uz; + + const auto sizeDefault = engine1.randomSize(); + expect(sizeDefault) <= static_cast(std::numeric_limits::max()); + + const auto text = engine1.randomString(8uz, 16uz); + expect(std::ranges::size(text) >= 8uz and std::ranges::size(text) <= 16uz); + + const auto bytes = engine1.randomBytes(32uz); + expect(std::ranges::size(bytes)) == 32uz; + + const auto items = std::vector{1, 2, 3, 4, 5}; + const auto picked = engine1.pickOne(items); + expect(picked >= 1 and picked <= 5); + }; + }; } diff --git a/tests/Memory/Memory.mpp b/tests/Memory/Memory.mpp index 102a05a0..e330595c 100644 --- a/tests/Memory/Memory.mpp +++ b/tests/Memory/Memory.mpp @@ -5,11 +5,11 @@ import CppUtils; namespace CppUtils::UnitTest::Memory { - auto _ = TestSuite{"Memory", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("moveRawPointer", [&] { + auto _ = testSuite("Memory").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test("moveRawPointer") = [&] { auto* ptr = new int{42}; auto value = CppUtils::Memory::moveRawPointer(ptr); - suite.expectEqual(value, 42); - }); - }}; + expect(value) == 42; + }; + }; } diff --git a/tests/Network/Client.mpp b/tests/Network/Client.mpp index 108d26d4..445a294b 100644 --- a/tests/Network/Client.mpp +++ b/tests/Network/Client.mpp @@ -6,9 +6,9 @@ import CppUtils.Network.Client; namespace CppUtils::UnitTest::Network::Client { - auto _ = TestSuite{"Network/Client", {"Logger", "Network/Socket"}, [](auto& suite) { - suite.addTest("Client Creation", [&] { + auto _ = testSuite("Network/Client").dependsOn("Logger", "Network/Socket") = [](TestSuite& suite) { + suite.test("Client Creation") = [&] { auto client = CppUtils::Network::Client{}; - }); - }}; + }; + }; } diff --git a/tests/Network/DNS.mpp b/tests/Network/DNS.mpp index 85775539..69a04e24 100644 --- a/tests/Network/DNS.mpp +++ b/tests/Network/DNS.mpp @@ -6,36 +6,36 @@ import CppUtils.Network.DNS; namespace CppUtils::UnitTest::Network::DNS { - auto _ = TestSuite{"Network/DNS", {"Logger"}, [](auto& suite) { + auto _ = testSuite("Network/DNS").dependsOn("Logger") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("Resolve Localhost", [&] { + suite.test("Resolve Localhost") = [&] { auto hostname = CppUtils::Network::DNS::getHostName(); Logger::print("Hostname: {}", hostname); - suite.expect(not std::ranges::empty(hostname)); + expect(not std::ranges::empty(hostname)); auto ips = CppUtils::Network::DNS::resolve("localhost"); - suite.expect(not std::ranges::empty(ips)); + expect(not std::ranges::empty(ips)); auto foundLoopback = false; for (const auto& ip : ips) if (ip == "127.0.0.1" or ip == "::1") foundLoopback = true; - suite.expect(foundLoopback); - }); + expect(foundLoopback); + }; - suite.addTest("Resolve whitehouse.gov", [&] { + suite.test("Resolve whitehouse.gov") = [&] { auto ips = CppUtils::Network::DNS::resolve("whitehouse.gov"); - suite.expect(not std::ranges::empty(ips)); + expect(not std::ranges::empty(ips)); Logger::print("whitehouse.gov resolved to:"); for (const auto& ip : ips) Logger::print("- {}", ip); - }); + }; - suite.addTest("Resolve one.one.one.one (Stable IP)", [&] { + suite.test("Resolve one.one.one.one (Stable IP)") = [&] { auto ips = CppUtils::Network::DNS::resolve("one.one.one.one"); - suite.expect(not std::ranges::empty(ips)); + expect(not std::ranges::empty(ips)); auto foundExpected = false; Logger::print("one.one.one.one resolved to:"); @@ -45,7 +45,7 @@ namespace CppUtils::UnitTest::Network::DNS foundExpected = true; Logger::print("- {}", ip); } - suite.expect(foundExpected); - }); - }}; + expect(foundExpected); + }; + }; } diff --git a/tests/Network/Http.mpp b/tests/Network/Http.mpp index 7ea04234..01eaf6dc 100644 --- a/tests/Network/Http.mpp +++ b/tests/Network/Http.mpp @@ -6,9 +6,9 @@ import CppUtils.Network.Http; namespace CppUtils::UnitTest::Network::Http { - auto _ = TestSuite{"Network/Http", {"Logger", "Thread/TryAsync", "Network/Server", "Network/Client", "Network/DNS", "Network/Socket"}, [](auto& suite) { - suite.addTest("Client/Server Exchange", [&] { - auto serverisReady = std::atomic_bool{false}; + auto _ = testSuite("Network/Http").dependsOn("Logger", "Thread/TryAsync", "Network/Server", "Network/Client", "Network/DNS", "Network/Socket") = [](TestSuite& suite) { + suite.test("Client/Server Exchange") = [&] { + auto serverisReady = std::atomic{0}; auto server = CppUtils::Network::Http::Server{8'080, [](const CppUtils::Network::Http::Request& request) -> CppUtils::Network::Http::Response { auto response = CppUtils::Network::Http::Response{}; response.body = "Hello " + request.path; @@ -16,31 +16,31 @@ namespace CppUtils::UnitTest::Network::Http }}; auto serverThread = CppUtils::Thread::tryAsync([&] { - serverisReady.store(true, std::memory_order_release); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); server.run(); }); auto clientThread = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Http::Client{}; auto connectionResult = client.connectIP("127.0.0.1", 8'080); - suite.expect(connectionResult); + expect(connectionResult); { auto response = client.get("/World"); - suite.expect(response); - suite.expectEqual(response->status, 200uz); - suite.expectEqual(response->body, "Hello /World"); + expect(response); + expect(response->status) == 200uz; + expect(response->body) == "Hello /World"; } { auto response = client.get("/Universe", {{"Connection", "close"}}); - suite.expect(response); - suite.expectEqual(response->status, 200uz); - suite.expectEqual(response->body, "Hello /Universe"); + expect(response); + expect(response->status) == 200uz; + expect(response->body) == "Hello /Universe"; } server.stop(); @@ -50,6 +50,6 @@ namespace CppUtils::UnitTest::Network::Http throw response.error(); if (auto response = serverThread.get(); not response) throw response.error(); - }); - }}; + }; + }; } diff --git a/tests/Network/Server.mpp b/tests/Network/Server.mpp index 10d7b6fb..84bb9fb8 100644 --- a/tests/Network/Server.mpp +++ b/tests/Network/Server.mpp @@ -6,36 +6,36 @@ import CppUtils.Network.Server; namespace CppUtils::UnitTest::Network::Server { - auto _ = TestSuite{"Network/Server", {"Logger", "Network/Socket"}, [](auto& suite) { + auto _ = testSuite("Network/Server").dependsOn("Logger", "Network/Socket") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("Server Creation", [&] { + suite.test("Server Creation") = [&] { auto server = CppUtils::Network::Server{}; - }); + }; - suite.addTest("Bind IP", [&] { + suite.test("Bind IP") = [&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - }); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + }; - suite.addTest("Listening", [&] { + suite.test("Listening") = [&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); Logger::print("Server: Waiting for connections on port 8080..."); - }); + }; - suite.addTest("Abort connection", [&] { + suite.test("Abort connection") = [&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); - suite.expect(server.setBlocking(false)); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); + expect(server.setBlocking(false)); Logger::print("Server: Waiting for connections on port 8080..."); auto clientSocket = server.accept(); - suite.expect(not clientSocket); - }); - }}; + expect(not clientSocket); + }; + }; } diff --git a/tests/Network/Socket.mpp b/tests/Network/Socket.mpp index e18c6c76..83a6e3c1 100644 --- a/tests/Network/Socket.mpp +++ b/tests/Network/Socket.mpp @@ -8,31 +8,31 @@ import CppUtils.Network.Client; namespace CppUtils::UnitTest::Network::Socket { - auto _ = TestSuite{"Network/Socket", {"Logger", "Thread/TryAsync"}, [](auto& suite) { + auto _ = testSuite("Network/Socket").dependsOn("Logger", "Thread/TryAsync") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("TCP: Accept connection", [&] { - auto serverisReady = std::atomic_bool{false}; + suite.test("TCP: Accept connection") = [&] { + auto serverisReady = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); Logger::print("Server: Waiting for connections on port 8080..."); - serverisReady.store(true, std::memory_order_release); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); auto clientSocket = server.accept(); - suite.expect(clientSocket); + expect(clientSocket); Logger::print("Server: A new client has joined"); }); auto clientConnecting = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Client{}; Logger::print("Client: Attempting to connect..."); - suite.expect(client.connect("", 8'080)); + expect(client.connect("", 8'080)); Logger::print("Client: Connected"); }); @@ -40,32 +40,32 @@ namespace CppUtils::UnitTest::Network::Socket throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); + }; - suite.addTest("TCP: Blocking", [&] { - auto serverisReady = std::atomic_bool{false}; + suite.test("TCP: Blocking") = [&] { + auto serverisReady = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); Logger::print("Server: Waiting for connections on port 8080..."); - serverisReady.store(true, std::memory_order_release); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); auto clientSocket = server.accept(); - suite.expect(clientSocket); + expect(clientSocket); Logger::print("Server: A new client has joined"); - suite.expect(clientSocket->setBlocking(true)); + expect(clientSocket->setBlocking(true)); Logger::print("Server: Blocking enabled"); }); auto clientConnecting = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Client{}; Logger::print("Client: Attempting to connect..."); - suite.expect(client.connect("", 8'080)); - suite.expect(client.setBlocking(true)); + expect(client.connect("", 8'080)); + expect(client.setBlocking(true)); Logger::print("Client: Blocking enabled"); }); @@ -73,54 +73,54 @@ namespace CppUtils::UnitTest::Network::Socket throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); + }; - suite.addTest("TCP: Send/receive message (int)", [&] { - auto serverisReady = std::atomic_bool{false}; + suite.test("TCP: Send/receive message (int)") = [&] { + auto serverisReady = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); Logger::print("Server: Waiting for connections on port 8080..."); - serverisReady.store(true, std::memory_order_release); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); auto clientSocketResult = server.accept(); - suite.expect(clientSocketResult); + expect(clientSocketResult); auto clientSocket = std::move(clientSocketResult.value()); Logger::print("Server: A new client has joined"); - suite.expect(clientSocket.setBlocking(true)); + expect(clientSocket.setBlocking(true)); Logger::print("Server: Socket set to blocking mode"); auto number = clientSocket.receive(); - suite.expect(number); + expect(number); Logger::print("Server: Message received: {}", number.value()); - suite.expectEqual(number.value(), 21); + expect(number.value() == 21); - suite.expect(clientSocket.send(42)); + expect(clientSocket.send(42)); Logger::print("Server: Message sent: 42"); Logger::print("Server: Closing"); }); auto clientConnecting = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Client{}; Logger::print("Client: Attempting to connect..."); - suite.expect(client.setBlocking(true)); - suite.expect(client.connect("", 8'080)); + expect(client.setBlocking(true)); + expect(client.connect("", 8'080)); Logger::print("Client: Connected to server"); - suite.expect(client.send(21)); + expect(client.send(21)); Logger::print("Client: Message sent: 21"); auto number = client.receive(); - suite.expect(number); + expect(number); Logger::print("Client: Message received: {}", number.value()); - suite.expectEqual(number.value(), 42); + expect(number.value()) == 42; Logger::print("Client: Closing"); }); @@ -129,62 +129,62 @@ namespace CppUtils::UnitTest::Network::Socket throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); + }; - suite.addTest("TCP: Send/receive message (std::string)", [&] { - auto serverisReady = std::atomic_bool{false}; + suite.test("TCP: Send/receive message (std::string)") = [&] { + auto serverisReady = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); Logger::print("Server: Waiting for connections on port 8080..."); - serverisReady.store(true, std::memory_order_release); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); auto clientSocketResult = server.accept(); - suite.expect(clientSocketResult); + expect(clientSocketResult); auto clientSocket = std::move(clientSocketResult.value()); Logger::print("Server: A new client has joined"); - suite.expect(clientSocket.setBlocking(true)); + expect(clientSocket.setBlocking(true)); Logger::print("Server: Socket set to blocking mode"); auto messageLength = clientSocket.receive(); - suite.expect(messageLength); + expect(messageLength); auto message = clientSocket.receive(messageLength.value()); - suite.expect(message); + expect(message); Logger::print(R"(Server: Message received: "{}")", message.value()); - suite.expectEqual(message.value(), "Hello server!"); + expect(message.value() == "Hello server!"); auto responseString = "Hello client!"sv; - suite.expect(clientSocket.send(std::ranges::size(responseString))); - suite.expect(clientSocket.send(responseString)); + expect(clientSocket.send(std::ranges::size(responseString))); + expect(clientSocket.send(responseString)); Logger::print(R"(Server: Message sent: "{}")", responseString); Logger::print("Server: Closing"); }); auto clientConnecting = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Client{}; Logger::print("Client: Attempting to connect..."); - suite.expect(client.setBlocking(true)); - suite.expect(client.connect("", 8'080)); + expect(client.setBlocking(true)); + expect(client.connect("", 8'080)); Logger::print("Client: Connected"); auto requestString = "Hello server!"sv; - suite.expect(client.send(std::ranges::size(requestString))); - suite.expect(client.send(requestString)); + expect(client.send(std::ranges::size(requestString))); + expect(client.send(requestString)); Logger::print(R"(Client: Message sent: "{}")", requestString); auto responseLength = client.receive(); - suite.expect(responseLength); + expect(responseLength); auto response = client.receive(responseLength.value()); - suite.expect(response); + expect(response); Logger::print(R"(Client: Message received: "{}")", response.value()); - suite.expectEqual(response.value(), "Hello client!"); + expect(response.value()) == "Hello client!"; Logger::print("Client: Closing"); }); @@ -193,78 +193,78 @@ namespace CppUtils::UnitTest::Network::Socket throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); + }; - suite.addTest("UDP: Send/Receive", [&] { - auto serverisReady = std::atomic_bool{false}; + suite.test("UDP: Send/Receive") = [&] { + auto serverisReady = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{CppUtils::Network::Socket::Domain::IPV4, CppUtils::Network::Socket::Type::UDP}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); - serverisReady.store(true, std::memory_order_release); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); auto result = server.receiveDatagram(1'024); - suite.expect(result); + expect(result); auto message = std::string{reinterpret_cast(std::ranges::data(result->data)), std::ranges::size(result->data)}; Logger::print(R"(Server: Received datagram: "{}")", message); - suite.expectEqual(message, "Hello server!"); + expect(message == "Hello server!"); Logger::print(R"(Server: Sending datagram: "Hello client!")"); - suite.expect(server.sendDatagram("Hello client!", result->endpoint.ip, result->endpoint.port)); + expect(server.sendDatagram("Hello client!", result->endpoint.ip, result->endpoint.port)); }); auto clientSending = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto socket = CppUtils::Network::Socket{CppUtils::Network::Socket::Domain::IPV4, CppUtils::Network::Socket::Type::UDP}; Logger::print(R"(Client: Sending datagram: "Hello server!")"); - suite.expect(socket.sendDatagram("Hello server!", "127.0.0.1", 8'080)); + expect(socket.sendDatagram("Hello server!", "127.0.0.1", 8'080)); auto result = socket.receiveDatagram(1'024); - suite.expect(result); + expect(result); auto message = std::string{reinterpret_cast(std::ranges::data(result->data)), std::ranges::size(result->data)}; Logger::print(R"(Client: Received datagram: "{}")", message); - suite.expectEqual(message, "Hello client!"); + expect(message == "Hello client!"); }); if (auto response = clientSending.get(); not response) throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); + }; - suite.addTest("isReadyForRead: Data available", [&] { - auto serverisReady = std::atomic_bool{false}; - auto clientReceived = std::atomic_bool{false}; + suite.test("isReadyForRead: Data available") = [&] { + auto serverisReady = std::atomic{0}; + auto clientReceived = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); - serverisReady.store(true, std::memory_order_release); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); auto clientSocketResult = server.accept(); - suite.expect(clientSocketResult); - suite.expect(clientSocketResult->send(123)); - clientReceived.wait(false); + expect(clientSocketResult); + expect(clientSocketResult->send(123)); + clientReceived.wait(0); }); auto clientConnecting = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Client{}; - suite.expect(client.connect("", 8'080)); - suite.expect(client.setBlocking(false)); + expect(client.connect("", 8'080)); + expect(client.setBlocking(false)); - suite.expect(client.isReadyForRead(5s)); + expect(client.isReadyForRead(5s)); auto data = client.receive(); - suite.expect(data); - suite.expectEqual(data.value(), 123); - clientReceived.store(true, std::memory_order_release); + expect(data); + expect(data.value()) == 123; + clientReceived.store(1, std::memory_order_release); clientReceived.notify_all(); }); @@ -272,135 +272,135 @@ namespace CppUtils::UnitTest::Network::Socket throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); + }; - suite.addTest("isReadyForRead: No data (timeout)", [&] { - auto serverisReady = std::atomic_bool{false}; + suite.test("isReadyForRead: No data (timeout)") = [&] { + auto serverisReady = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); - serverisReady.store(true, std::memory_order_release); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); auto clientSocket = server.accept(); - suite.expect(clientSocket); + expect(clientSocket); std::this_thread::sleep_for(1s); }); auto clientConnecting = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Client{}; - suite.expect(client.connect("", 8'080)); - suite.expect(client.setBlocking(false)); - suite.expect(not client.isReadyForRead(100ms)); + expect(client.connect("", 8'080)); + expect(client.setBlocking(false)); + expect(not client.isReadyForRead(100ms)); }); if (auto response = clientConnecting.get(); not response) throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); + }; - suite.addTest("Non-blocking: Would block", [&] { - auto serverisReady = std::atomic_bool{false}; + suite.test("Non-blocking: Would block") = [&] { + auto serverisReady = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); - serverisReady.store(true, std::memory_order_release); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); auto clientSocket = server.accept(); - suite.expect(clientSocket); + expect(clientSocket); std::this_thread::sleep_for(500ms); }); auto clientConnecting = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Client{}; - suite.expect(client.connect("", 8'080)); - suite.expect(client.setBlocking(false)); + expect(client.connect("", 8'080)); + expect(client.setBlocking(false)); auto result = client.receive(); - suite.expect(not result); - suite.expectEqual(result.error(), std::make_error_code(std::errc::resource_unavailable_try_again)); + expect(not result); + expect(result.error()) == std::make_error_code(std::errc::resource_unavailable_try_again); }); if (auto response = clientConnecting.get(); not response) throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); + }; - suite.addTest("Socket health check", [&] { - auto serverisReady = std::atomic_bool{false}; + suite.test("Socket health check") = [&] { + auto serverisReady = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); - serverisReady.store(true, std::memory_order_release); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); - suite.expect(server.accept()); + expect(server.accept()); }); auto clientConnecting = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Client{}; - suite.expect(client.connect("", 8'080)); + expect(client.connect("", 8'080)); - suite.expect(client.checkPendingError()); + expect(client.checkPendingError()); }); if (auto response = clientConnecting.get(); not response) throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); + }; - suite.addTest("TCP: Client detects server disconnect", [&] { - auto serverisReady = std::atomic_bool{false}; - auto serverClosedConnection = std::atomic_bool{false}; + suite.test("TCP: Client detects server disconnect") = [&] { + auto serverisReady = std::atomic{0}; + auto serverClosedConnection = std::atomic{0}; auto serverListening = CppUtils::Thread::tryAsync([&] { auto server = CppUtils::Network::Server{}; - suite.expect(server.setReuseAddress(true)); - suite.expect(server.bind(8'080)); - suite.expect(server.listen()); - serverisReady.store(true, std::memory_order_release); + expect(server.setReuseAddress(true)); + expect(server.bind(8'080)); + expect(server.listen()); + serverisReady.store(1, std::memory_order_release); serverisReady.notify_all(); { auto clientSocket = server.accept(); - suite.expect(clientSocket); + expect(clientSocket); } - serverClosedConnection.store(true, std::memory_order_release); + serverClosedConnection.store(1, std::memory_order_release); serverClosedConnection.notify_all(); }); auto clientConnecting = CppUtils::Thread::tryAsync([&] { - serverisReady.wait(false); + serverisReady.wait(0); auto client = CppUtils::Network::Client{}; - suite.expect(client.connect("", 8'080)); + expect(client.connect("", 8'080)); - serverClosedConnection.wait(false); - suite.expect(client.isReadyForRead(1s)); + serverClosedConnection.wait(0); + expect(client.isReadyForRead(1s)); auto receiveResult = client.receive(); - suite.expect(not receiveResult); - suite.expect(CppUtils::Network::Socket::isDisconnectError(receiveResult.error())); + expect(not receiveResult); + expect(CppUtils::Network::Socket::isDisconnectError(receiveResult.error())); }); if (auto response = clientConnecting.get(); not response) throw response.error(); if (auto response = serverListening.get(); not response) throw response.error(); - }); - }}; + }; + }; } diff --git a/tests/Pattern/Pattern.mpp b/tests/Pattern/Pattern.mpp index da51b29e..98e37f1d 100644 --- a/tests/Pattern/Pattern.mpp +++ b/tests/Pattern/Pattern.mpp @@ -10,17 +10,17 @@ namespace CppUtils::UnitTest::Pattern int value = 0; }; - auto _ = TestSuite{"Pattern", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("Singleton", [&] { + auto _ = testSuite("Pattern").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test("Singleton") = [&] { auto& instance1 = CppUtils::Pattern::Singleton::get(); auto& instance2 = CppUtils::Pattern::Singleton::get(); instance1.value = 42; - suite.expectEqual(instance2.value, 42); - suite.expectEqual(std::addressof(instance1), std::addressof(instance2)); - }); + expect(instance2.value) == 42; + expect(std::addressof(instance1)) == std::addressof(instance2); + }; - suite.addTest("Multiton", [&] { + suite.test("Multiton") = [&] { constexpr auto keyA = 1; constexpr auto keyB = 2; @@ -31,11 +31,11 @@ namespace CppUtils::UnitTest::Pattern instanceA.value = 10; instanceB.value = 20; - suite.expectEqual(instanceA.value, 10); - suite.expectEqual(instanceB.value, 20); - suite.expectEqual(instanceA2.value, 10); - suite.expectEqual(std::addressof(instanceA), std::addressof(instanceA2)); - suite.expect(std::addressof(instanceA) != std::addressof(instanceB)); - }); - }}; + expect(instanceA.value) == 10; + expect(instanceB.value) == 20; + expect(instanceA2.value) == 10; + expect(std::addressof(instanceA)) == std::addressof(instanceA2); + expect(std::addressof(instanceA)) != std::addressof(instanceB); + }; + }; } diff --git a/tests/Ranges/Drain.mpp b/tests/Ranges/Drain.mpp index 09fcbd61..d4f58cb5 100644 --- a/tests/Ranges/Drain.mpp +++ b/tests/Ranges/Drain.mpp @@ -5,8 +5,8 @@ import CppUtils; namespace CppUtils::UnitTest::Ranges::Drain { - auto _ = TestSuite{"Ranges/Drain", {}, [](TestSuite& suite) { - suite.addTest("Pipe syntax", [&] { + auto _ = testSuite("Ranges/Drain") = [](TestSuite& suite) { + suite.test("Pipe syntax") = [&] { auto count = 0; const auto numbers = std::vector{1, 2, 3, 4, 5}; @@ -17,10 +17,10 @@ namespace CppUtils::UnitTest::Ranges::Drain }) | CppUtils::Ranges::drain; - suite.expectEqual(count, 15); - }); + expect(count) == 15; + }; - suite.addTest("Direct call", [&] { + suite.test("Direct call") = [&] { auto count = 0; const auto numbers = std::vector{10, 20, 30}; @@ -30,10 +30,10 @@ namespace CppUtils::UnitTest::Ranges::Drain return value; })); - suite.expectEqual(count, 60); - }); + expect(count) == 60; + }; - suite.addTest("Constexpr evaluation", [&] { + suite.test("Constexpr evaluation") = [&] { constexpr auto sum = [] { auto count = 0; const auto numbers = std::array{1, 2, 3}; @@ -46,6 +46,6 @@ namespace CppUtils::UnitTest::Ranges::Drain return count; }(); static_assert(sum == 6); - }); - }}; + }; + }; } diff --git a/tests/Ranges/Expected.mpp b/tests/Ranges/Expected.mpp index 39eff05a..f66fe0b3 100644 --- a/tests/Ranges/Expected.mpp +++ b/tests/Ranges/Expected.mpp @@ -5,11 +5,11 @@ import CppUtils; namespace CppUtils::UnitTest::Ranges::Expected { - auto _ = TestSuite{"Ranges/Expected", {}, [](TestSuite& suite) { + auto _ = testSuite("Ranges/Expected") = [](TestSuite& suite) { using namespace std::literals; using Expected = std::expected; - suite.addTest("filterValues", [&] { + suite.test("filterValues") = [&] { const auto inputs = std::array{ 10, std::unexpected{"error 1"s}, @@ -21,13 +21,13 @@ namespace CppUtils::UnitTest::Ranges::Expected | CppUtils::Ranges::filterValues() | std::ranges::to>(); - suite.expectEqual(std::ranges::size(values), 3uz); - suite.expectEqual(values[0], 10); - suite.expectEqual(values[1], 20); - suite.expectEqual(values[2], 30); - }); + expect(std::ranges::size(values)) == 3uz; + expect(values[0]) == 10; + expect(values[1]) == 20; + expect(values[2]) == 30; + }; - suite.addTest("filterErrors", [&] { + suite.test("filterErrors") = [&] { const auto inputs = std::array{ 10, std::unexpected{"error 1"s}, @@ -39,12 +39,12 @@ namespace CppUtils::UnitTest::Ranges::Expected | CppUtils::Ranges::filterErrors() | std::ranges::to>(); - suite.expectEqual(std::ranges::size(errors), 2uz); - suite.expectEqual(errors[0], "error 1"s); - suite.expectEqual(errors[1], "error 2"s); - }); + expect(std::ranges::size(errors)) == 2uz; + expect(errors[0]) == "error 1"s; + expect(errors[1]) == "error 2"s; + }; - suite.addTest("valueOr", [&] { + suite.test("valueOr") = [&] { const auto inputs = std::array{ 10, std::unexpected{"error"s}, @@ -54,13 +54,13 @@ namespace CppUtils::UnitTest::Ranges::Expected | CppUtils::Ranges::valueOr(-1) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(values), 3uz); - suite.expectEqual(values[0], 10); - suite.expectEqual(values[1], -1); - suite.expectEqual(values[2], 30); - }); + expect(std::ranges::size(values)) == 3uz; + expect(values[0]) == 10; + expect(values[1]) == -1; + expect(values[2]) == 30; + }; - suite.addTest("recover", [&] { + suite.test("recover") = [&] { const auto inputs = std::array{ 10, std::unexpected{"42"s}, @@ -72,13 +72,13 @@ namespace CppUtils::UnitTest::Ranges::Expected }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(values), 3uz); - suite.expectEqual(values[0], 10); - suite.expectEqual(values[1], 42); - suite.expectEqual(values[2], 30); - }); + expect(std::ranges::size(values)) == 3uz; + expect(values[0]) == 10; + expect(values[1]) == 42; + expect(values[2]) == 30; + }; - suite.addTest("inspectError", [&] { + suite.test("inspectError") = [&] { const auto inputs = std::array{ 10, std::unexpected{"error"s}, @@ -87,17 +87,17 @@ namespace CppUtils::UnitTest::Ranges::Expected auto capturedErrors = std::vector{}; auto count = 0uz; - for (const auto& item : inputs | CppUtils::Ranges::inspectError([&](const std::string& error) { + for ([[maybe_unused]] const auto& item : inputs | CppUtils::Ranges::inspectError([&](const std::string& error) { capturedErrors.push_back(error); })) ++count; - suite.expectEqual(count, 3uz); - suite.expectEqual(std::ranges::size(capturedErrors), 1uz); - suite.expectEqual(capturedErrors[0], "error"s); - }); + expect(count) == 3uz; + expect(std::ranges::size(capturedErrors)) == 1uz; + expect(capturedErrors[0]) == "error"; + }; - suite.addTest("routeErrors", [&] { + suite.test("routeErrors") = [&] { const auto inputs = std::array{ 10, std::unexpected{"error 1"s}, @@ -113,14 +113,14 @@ namespace CppUtils::UnitTest::Ranges::Expected }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(values), 3uz); - suite.expectEqual(values[0], 10); - suite.expectEqual(values[1], 20); - suite.expectEqual(values[2], 30); + expect(std::ranges::size(values)) == 3uz; + expect(values[0]) == 10; + expect(values[1]) == 20; + expect(values[2]) == 30; - suite.expectEqual(std::ranges::size(capturedErrors), 2uz); - suite.expectEqual(capturedErrors[0], "error 1"s); - suite.expectEqual(capturedErrors[1], "error 2"s); - }); - }}; + expect(std::ranges::size(capturedErrors)) == 2uz; + expect(capturedErrors[0]) == "error 1"; + expect(capturedErrors[1]) == "error 2"; + }; + }; } diff --git a/tests/Ranges/Inspect.mpp b/tests/Ranges/Inspect.mpp index bdf3defb..87c3192f 100644 --- a/tests/Ranges/Inspect.mpp +++ b/tests/Ranges/Inspect.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Ranges::Inspect { - auto _ = TestSuite{"Ranges/Inspect", {}, [](TestSuite& suite) { + auto _ = testSuite("Ranges/Inspect") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("Pipe syntax", [&] { + suite.test("Pipe syntax") = [&] { const auto numbers = std::vector{1, 2, 3, 4, 5}; auto observed = std::vector{}; @@ -17,11 +17,11 @@ namespace CppUtils::UnitTest::Ranges::Inspect | std::views::transform([](int value) { return value * 10; }) | std::ranges::to>(); - suite.expectEqual(observed, numbers); - suite.expectEqual(results, std::vector{10, 20, 30, 40, 50}); - }); + expect(observed) == numbers; + expect(results) == std::vector{10, 20, 30, 40, 50}; + }; - suite.addTest("Direct call", [&] { + suite.test("Direct call") = [&] { const auto words = std::vector{"hello", "world", "!"}; auto count = 0uz; @@ -31,11 +31,11 @@ namespace CppUtils::UnitTest::Ranges::Inspect const auto collected = view | std::ranges::to>(); - suite.expectEqual(count, 3uz); - suite.expectEqual(collected, words); - }); + expect(count) == 3uz; + expect(collected) == words; + }; - suite.addTest("Mutable elements", [&] { + suite.test("Mutable elements") = [&] { struct Item final { int id = 0; @@ -48,12 +48,12 @@ namespace CppUtils::UnitTest::Ranges::Inspect for (auto& item : items | CppUtils::Ranges::inspect([&seenIds](const Item& itemElement) { seenIds.push_back(itemElement.id); })) item.name += "_processed"; - suite.expectEqual(seenIds, std::vector{1, 2}); - suite.expectEqual(items[0].name, "alpha_processed"); - suite.expectEqual(items[1].name, "beta_processed"); - }); + expect(seenIds) == std::vector{1, 2}; + expect(items[0].name) == "alpha_processed"; + expect(items[1].name) == "beta_processed"; + }; - suite.addTest("Chained inspect", [&] { + suite.test("Chained inspect") = [&] { const auto inputs = std::vector{10, 20, 30}; auto step1Logs = std::vector{}; auto step2Logs = std::vector{}; @@ -64,9 +64,9 @@ namespace CppUtils::UnitTest::Ranges::Inspect | CppUtils::Ranges::inspect([&step2Logs](int value) { step2Logs.push_back(value); }) | std::ranges::to>(); - suite.expectEqual(step1Logs, std::vector{10, 20, 30}); - suite.expectEqual(step2Logs, std::vector{20, 40, 60}); - suite.expectEqual(results, std::vector{20, 40, 60}); - }); - }}; + expect(step1Logs) == std::vector{10, 20, 30}; + expect(step2Logs) == std::vector{20, 40, 60}; + expect(results) == std::vector{20, 40, 60}; + }; + }; } diff --git a/tests/Ranges/Parallel.mpp b/tests/Ranges/Parallel.mpp index 25b1a098..7cf79215 100644 --- a/tests/Ranges/Parallel.mpp +++ b/tests/Ranges/Parallel.mpp @@ -5,80 +5,75 @@ import CppUtils; namespace CppUtils::UnitTest::Ranges::Parallel { - auto _ = TestSuite{"Ranges/Parallel", {}, [](TestSuite& suite) { - suite.addTest("parallel scalar transformation", [&] { + auto _ = testSuite("Ranges/Parallel") = [](TestSuite& suite) { + suite.test("parallel scalar transformation") = [&] { auto threadPool = Thread::ThreadPool{4}; const auto inputs = std::vector{1, 2, 3, 4, 5}; const auto results = inputs | CppUtils::Ranges::parallel(threadPool, [](int value) { - return value * 10; - }) - | std::ranges::to>(); + return value * 10; + }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(results), 5uz); - suite.expectEqual(results, std::vector{10, 20, 30, 40, 50}); - }); + expect(std::ranges::size(results)) == 5uz; + expect(results) == std::vector{10, 20, 30, 40, 50}; + }; - suite.addTest("parallel empty range", [&] { + suite.test("parallel empty range") = [&] { auto threadPool = Thread::ThreadPool{2}; const auto inputs = std::vector{}; const auto results = inputs | CppUtils::Ranges::parallel(threadPool, [](const std::string& text) { - return text + "!"; - }) - | std::ranges::to>(); + return text + "!"; + }) | std::ranges::to>(); - suite.expect(std::ranges::empty(results)); - }); + expect(std::ranges::empty(results)); + }; - suite.addTest("parallelChunk simple transformation", [&] { + suite.test("parallelChunk simple transformation") = [&] { auto threadPool = Thread::ThreadPool{4}; const auto inputs = std::vector{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; const auto results = inputs | CppUtils::Ranges::parallelChunk(threadPool, 3, [](std::vector chunk) { - for (auto& value : chunk) - value *= 2; - return chunk; - }) - | std::ranges::to>(); + for (auto& value : chunk) + value *= 2; + return chunk; + }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(results), 10uz); - suite.expectEqual(results, std::vector{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}); - }); + expect(std::ranges::size(results)) == 10uz; + expect(results) == std::vector{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}; + }; - suite.addTest("parallelChunk empty range", [&] { + suite.test("parallelChunk empty range") = [&] { auto threadPool = Thread::ThreadPool{2}; const auto inputs = std::vector{}; const auto results = inputs | CppUtils::Ranges::parallelChunk(threadPool, 5, [](std::vector chunk) { - return chunk; - }) - | std::ranges::to>(); + return chunk; + }) | std::ranges::to>(); - suite.expect(std::ranges::empty(results)); - }); + expect(std::ranges::empty(results)); + }; - suite.addTest("parallelChunk input smaller than chunk size", [&] { + suite.test("parallelChunk input smaller than chunk size") = [&] { auto threadPool = Thread::ThreadPool{2}; const auto inputs = std::vector{10, 20, 30}; const auto results = inputs | CppUtils::Ranges::parallelChunk(threadPool, 100, [](std::vector chunk) { - for (auto& value : chunk) - value += 1; - return chunk; - }) - | std::ranges::to>(); + for (auto& value : chunk) + value += 1; + return chunk; + }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(results), 3uz); - suite.expectEqual(results, std::vector{11, 21, 31}); - }); + expect(std::ranges::size(results)) == 3uz; + expect(results) == std::vector{11, 21, 31}; + }; - suite.addTest("parallelChunk large dataset order and count", [&] { + suite.test("parallelChunk large dataset order and count") = [&] { auto threadPool = Thread::ThreadPool{4}; constexpr auto totalItems = 5000uz; auto inputs = std::vector(totalItems); @@ -86,37 +81,35 @@ namespace CppUtils::UnitTest::Ranges::Parallel const auto results = inputs | CppUtils::Ranges::parallelChunk(threadPool, 250, [](std::vector chunk) { - for (auto& value : chunk) - value *= 10; - return chunk; - }) - | std::ranges::to>(); + for (auto& value : chunk) + value *= 10; + return chunk; + }) | std::ranges::to>(); - suite.expectEqual(std::ranges::size(results), totalItems); + expect(std::ranges::size(results)) == totalItems; for (auto index = 0uz; index < totalItems; ++index) - suite.expectEqual(results[index], index * 10); - }); + expect(results[index]) == index * 10uz; + }; - suite.addTest("parallelChunk type transformation", [&] { + suite.test("parallelChunk type transformation") = [&] { auto threadPool = Thread::ThreadPool{3}; const auto inputs = std::vector{1, 2, 3, 4, 5}; const auto results = inputs | CppUtils::Ranges::parallelChunk(threadPool, 2, [](std::vector chunk) { - auto stringChunk = std::vector{}; - stringChunk.reserve(std::ranges::size(chunk)); - for (const auto value : chunk) - stringChunk.push_back(std::format("item_{}", value)); - return stringChunk; - }) - | std::ranges::to>(); - - suite.expectEqual(std::ranges::size(results), 5uz); - suite.expectEqual(results[0], "item_1"); - suite.expectEqual(results[1], "item_2"); - suite.expectEqual(results[2], "item_3"); - suite.expectEqual(results[3], "item_4"); - suite.expectEqual(results[4], "item_5"); - }); - }}; + auto stringChunk = std::vector{}; + stringChunk.reserve(std::ranges::size(chunk)); + for (const auto value : chunk) + stringChunk.push_back(std::format("item_{}", value)); + return stringChunk; + }) | std::ranges::to>(); + + expect(std::ranges::size(results)) == 5uz; + expect(results[0]) == "item_1"; + expect(results[1]) == "item_2"; + expect(results[2]) == "item_3"; + expect(results[3]) == "item_4"; + expect(results[4]) == "item_5"; + }; + }; } diff --git a/tests/Stl/format.mpp b/tests/Stl/format.mpp index 79008281..81b992da 100644 --- a/tests/Stl/format.mpp +++ b/tests/Stl/format.mpp @@ -9,13 +9,13 @@ import CppUtils; namespace CppUtils::UnitTest::Stl::Format { /* - auto _ = TestSuite{"Stl/Format", [](TestSuite& suite) { + auto _ = testSuite("Stl/Format") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("empty string", [&] { + suite.test("empty string") = [&] { constexpr auto result = ::Stl::format(""); - suite.expectEqual(result, ""sv); - }); - }}; + expect(result) == ""; + }; + }; */ } diff --git a/tests/String/Concept.mpp b/tests/String/Concept.mpp new file mode 100644 index 00000000..f4cdc074 --- /dev/null +++ b/tests/String/Concept.mpp @@ -0,0 +1,72 @@ +export module CppUtils.UnitTests.String.Concept; + +import std; +import CppUtils; + +namespace CppUtils::UnitTest::String::Concept +{ + auto _ = testSuite("String/Concept").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test<[] { + static_assert(std::same_as, char>); + static_assert(std::same_as, char>); + static_assert(std::same_as, char>); + static_assert(std::same_as, wchar_t>); + static_assert(std::same_as, char>); + static_assert(std::same_as, char32_t>); + }>("CharacterType"); + + suite.test<[] { + static_assert(CppUtils::String::CharacterPointerOrArray); + static_assert(CppUtils::String::CharacterPointerOrArray); + static_assert(CppUtils::String::CharacterPointerOrArray); + static_assert(CppUtils::String::CharacterPointerOrArray); + static_assert(CppUtils::String::CharacterPointerOrArray); + static_assert(CppUtils::String::CharacterPointerOrArray); + static_assert(not CppUtils::String::CharacterPointerOrArray); + static_assert(not CppUtils::String::CharacterPointerOrArray); + static_assert(not CppUtils::String::CharacterPointerOrArray); + }>("CharacterPointerOrArray"); + + suite.test<[] { + static_assert(CppUtils::String::CharacterSequence); + static_assert(CppUtils::String::CharacterSequence); + static_assert(CppUtils::String::CharacterSequence); + static_assert(CppUtils::String::CharacterSequence); + static_assert(CppUtils::String::CharacterSequence>); + static_assert(CppUtils::String::CharacterSequence>); + static_assert(not CppUtils::String::CharacterSequence>); + static_assert(not CppUtils::String::CharacterSequence); + }>("CharacterSequence"); + + suite.test<[] { + static_assert(CppUtils::String::Text); + static_assert(CppUtils::String::Text); + static_assert(CppUtils::String::Text); + static_assert(CppUtils::String::GenericText); + static_assert(CppUtils::String::GenericText); + static_assert(not CppUtils::String::GenericText>); + }>("Text and GenericText"); + + suite.test<[] { + static_assert(CppUtils::String::Formattable); + static_assert(CppUtils::String::Formattable); + static_assert(CppUtils::String::DirectlyPrintable); + static_assert(CppUtils::String::DirectlyPrintable); + static_assert(CppUtils::String::DirectlyPrintable); + static_assert(CppUtils::String::DirectlyPrintable); + static_assert(not CppUtils::String::DirectlyPrintable>); + static_assert(not CppUtils::String::DirectlyPrintable>); + static_assert(not CppUtils::String::DirectlyPrintable>); + static_assert(CppUtils::String::ExtensivelyPrintable>); + static_assert(CppUtils::String::ExtensivelyPrintable>); + static_assert(CppUtils::String::ExtensivelyPrintable>); + static_assert(CppUtils::String::ExtensivelyPrintable>); + static_assert(CppUtils::String::ExtensivelyPrintable>); + static_assert(CppUtils::String::ExtensivelyPrintable>); + static_assert(CppUtils::String::ExtensivelyPrintable>); + static_assert(CppUtils::String::ExtensivelyPrintable>); + static_assert(not CppUtils::String::ExtensivelyPrintable>); + static_assert(not CppUtils::String::ExtensivelyPrintable>); + }>("Formattable, DirectlyPrintable and ExtensivelyPrintable"); + }; +} diff --git a/tests/String/Encoding.mpp b/tests/String/Encoding.mpp index 00d2cd8a..f5085fcf 100644 --- a/tests/String/Encoding.mpp +++ b/tests/String/Encoding.mpp @@ -5,22 +5,22 @@ import CppUtils; namespace CppUtils::UnitTest::String::Encoding { - auto _ = TestSuite{"String/Encoding", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("String/Encoding").dependsOn("Logger") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("toUtf8(char32_t)", [&] { + suite.test("toUtf8(char32_t)") = [&] { // 1 octet (0x00 - 0x7F) { static_assert(char32_t{0x24} == U'$'); auto result = CppUtils::String::toUtf8(char32_t{0x24}); Logger::print("toUtf8(0x24) -> {}", result); - suite.expectEqual(result, "\x24"); + expect(result) == "\x24"; result = CppUtils::String::toUtf8(U'$'); Logger::print("toUtf8(U'$') -> {}", result); - suite.expectEqual(result, "\x24"); + expect(result) == "\x24"; } { @@ -28,11 +28,11 @@ namespace CppUtils::UnitTest::String::Encoding auto result = CppUtils::String::toUtf8(char32_t{0x41}); Logger::print("toUtf8(0x41) -> {}", result); - suite.expectEqual(result, "A"); + expect(result) == "A"; result = CppUtils::String::toUtf8(U'A'); Logger::print("toUtf8(U'A') -> {}", result); - suite.expectEqual(result, "A"); + expect(result) == "A"; } // 2 octet (0x80 - 0x7FF) @@ -41,11 +41,11 @@ namespace CppUtils::UnitTest::String::Encoding auto result = CppUtils::String::toUtf8(char32_t{0xA2}); Logger::print("toUtf8(0xA2) -> {}", result); - suite.expectEqual(result, "\xC2\xA2"); + expect(result) == "\xC2\xA2"; result = CppUtils::String::toUtf8(U'¢'); Logger::print("toUtf8(U'¢') -> {}", result); - suite.expectEqual(result, "\xC2\xA2"); + expect(result) == "\xC2\xA2"; } { @@ -53,11 +53,11 @@ namespace CppUtils::UnitTest::String::Encoding auto result = CppUtils::String::toUtf8(char32_t{0x3'B1}); Logger::print("toUtf8(0x3B1) -> {}", result); - suite.expectEqual(result, "\xCE\xB1"); + expect(result) == "\xCE\xB1"; result = CppUtils::String::toUtf8(U'α'); Logger::print("toUtf8(U'α') -> {}", result); - suite.expectEqual(result, "\xCE\xB1"); + expect(result) == "\xCE\xB1"; } // 3 octet (0x800 - 0xFFFF) @@ -66,11 +66,11 @@ namespace CppUtils::UnitTest::String::Encoding auto result = CppUtils::String::toUtf8(char32_t{0x20'AC}); Logger::print("toUtf8(0x20AC) -> {}", result); - suite.expectEqual(result, "\xE2\x82\xAC"); + expect(result) == "\xE2\x82\xAC"; result = CppUtils::String::toUtf8(U'€'); Logger::print("toUtf8(U'€') -> {}", result); - suite.expectEqual(result, "\xE2\x82\xAC"); + expect(result) == "\xE2\x82\xAC"; } { @@ -78,11 +78,11 @@ namespace CppUtils::UnitTest::String::Encoding auto result = CppUtils::String::toUtf8(char32_t{0x30'93}); Logger::print("toUtf8(0x3093) -> {}", result); - suite.expectEqual(result, "\xE3\x82\x93"); + expect(result) == "\xE3\x82\x93"; result = CppUtils::String::toUtf8(U'ん'); Logger::print("toUtf8(U'ん') -> {}", result); - suite.expectEqual(result, "\xE3\x82\x93"); + expect(result) == "\xE3\x82\x93"; } // 4 octet (0x10000 - 0x10FFFF) @@ -91,11 +91,11 @@ namespace CppUtils::UnitTest::String::Encoding auto result = CppUtils::String::toUtf8(char32_t{0x1'03'48}); Logger::print("toUtf8(0x10348) -> {}", result); - suite.expectEqual(result, "\xF0\x90\x8D\x88"); + expect(result) == "\xF0\x90\x8D\x88"; result = CppUtils::String::toUtf8(U'𐍈'); Logger::print("toUtf8(U'𐍈') -> {}", result); - suite.expectEqual(result, "\xF0\x90\x8D\x88"); + expect(result) == "\xF0\x90\x8D\x88"; } { @@ -103,85 +103,85 @@ namespace CppUtils::UnitTest::String::Encoding auto result = CppUtils::String::toUtf8(char32_t{0x1'F6'00}); Logger::print("toUtf8(0x1F600) -> {}", result); - suite.expectEqual(result, "\xF0\x9F\x98\x80"); + expect(result) == "\xF0\x9F\x98\x80"; result = CppUtils::String::toUtf8(U'😀'); Logger::print("toUtf8(U'😀') -> {}", result); - suite.expectEqual(result, "\xF0\x9F\x98\x80"); + expect(result) == "\xF0\x9F\x98\x80"; } - }); + }; - suite.addTest("toUtf8(std::u32string_view)", [&] { + suite.test("toUtf8(std::u32string_view)") = [&] { // 1 octet { auto result = CppUtils::String::toUtf8(U"$"sv); Logger::print("toUtf8(U\"$\") -> {}", result); - suite.expectEqual(result, "\x24"); + expect(result) == "\x24"; } { auto result = CppUtils::String::toUtf8(U"A"sv); Logger::print("toUtf8(U\"A\") -> {}", result); - suite.expectEqual(result, "A"); + expect(result) == "A"; } // 2 octet { auto result = CppUtils::String::toUtf8(U"¢"sv); Logger::print("toUtf8(U\"¢\") -> {}", result); - suite.expectEqual(result, "\xC2\xA2"); + expect(result) == "\xC2\xA2"; } { auto result = CppUtils::String::toUtf8(U"α"sv); Logger::print("toUtf8(U\"α\") -> {}", result); - suite.expectEqual(result, "\xCE\xB1"); + expect(result) == "\xCE\xB1"; } // 3 octet { auto result = CppUtils::String::toUtf8(U"€"sv); Logger::print("toUtf8(U\"€\") -> {}", result); - suite.expectEqual(result, "\xE2\x82\xAC"); + expect(result) == "\xE2\x82\xAC"; } { auto result = CppUtils::String::toUtf8(U"ん"sv); Logger::print("toUtf8(U\"ん\") -> {}", result); - suite.expectEqual(result, "\xE3\x82\x93"); + expect(result) == "\xE3\x82\x93"; } // 4 octet { auto result = CppUtils::String::toUtf8(U"𐍈"sv); Logger::print("toUtf8(U\"𐍈\") -> {}", result); - suite.expectEqual(result, "\xF0\x90\x8D\x88"); + expect(result) == "\xF0\x90\x8D\x88"; } { auto result = CppUtils::String::toUtf8(U"😀"sv); Logger::print("toUtf8(U\"😀\") -> {}", result); - suite.expectEqual(result, "\xF0\x9F\x98\x80"); + expect(result) == "\xF0\x9F\x98\x80"; } // Combined { auto result = CppUtils::String::toUtf8(U"Hello World!"sv); Logger::print("toUtf8(U\"Hello World!\") -> {}", result); - suite.expectEqual(result, "Hello World!"); + expect(result) == "Hello World!"; } { auto result = CppUtils::String::toUtf8(U"$A¢α€ん𐍈😀"sv); Logger::print("toUtf8(U\"$A¢α€ん𐍈😀\") -> {}", result); - suite.expectEqual(result, "$A\xC2\xA2\xCE\xB1\xE2\x82\xAC\xE3\x82\x93\xF0\x90\x8D\x88\xF0\x9F\x98\x80"); + expect(result) == "$A\xC2\xA2\xCE\xB1\xE2\x82\xAC\xE3\x82\x93\xF0\x90\x8D\x88\xF0\x9F\x98\x80"; } { auto result = CppUtils::String::toUtf8(U"A€😀"sv); Logger::print("toUtf8(U\"A€😀\") -> {}", result); - suite.expectEqual(result, "A\xE2\x82\xAC\xF0\x9F\x98\x80"); + expect(result) == "A\xE2\x82\xAC\xF0\x9F\x98\x80"; } - }); - }}; + }; + }; } diff --git a/tests/String/Utility.mpp b/tests/String/Utility.mpp index 3d090894..181499ad 100644 --- a/tests/String/Utility.mpp +++ b/tests/String/Utility.mpp @@ -5,108 +5,147 @@ import CppUtils; namespace CppUtils::UnitTest::String::Utility { - auto _ = TestSuite{"String/Utility", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("String/Utility").dependsOn("Logger") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("trimString", [&] { + suite.test("trimString") = [&] { auto string = "\n \t Hello World!\n \t "; auto leftTrimString = CppUtils::String::leftTrimString(string); auto rightTrimString = CppUtils::String::rightTrimString(string); auto trimString = CppUtils::String::trimString(string); Logger::print(R"("{}")", leftTrimString); - suite.expectEqual(leftTrimString, "Hello World!\n \t "); + expect(leftTrimString) == "Hello World!\n \t "; Logger::print(R"("{}")", rightTrimString); - suite.expectEqual(rightTrimString, "\n \t Hello World!"); + expect(rightTrimString) == "\n \t Hello World!"; Logger::print(R"("{}")", trimString); - suite.expectEqual(trimString, "Hello World!"); - }); + expect(trimString) == "Hello World!"; + }; - suite.addTest("getPrintableChar", [&] { + suite.test("getPrintableChar") = [&] { auto output = ""s; for (char c = 0; c >= 0; ++c) output += CppUtils::String::getPrintableChar(c) + ' '; Logger::print("{}", output); - }); - - suite.addTest("getNumberLength", [&] { - suite.expectEqual(CppUtils::String::getNumberLength(0), 1uz); - suite.expectEqual(CppUtils::String::getNumberLength(1), 1uz); - suite.expectEqual(CppUtils::String::getNumberLength(2), 1uz); - suite.expectEqual(CppUtils::String::getNumberLength(10), 2uz); - - suite.expectEqual(CppUtils::String::getNumberLength(-1), 2uz); - suite.expectEqual(CppUtils::String::getNumberLength(-2), 2uz); - suite.expectEqual(CppUtils::String::getNumberLength(-10), 3uz); - - suite.expectEqual(CppUtils::String::getNumberLength(0, 2), 1uz); - suite.expectEqual(CppUtils::String::getNumberLength(1, 2), 1uz); - suite.expectEqual(CppUtils::String::getNumberLength(2, 2), 2uz); - suite.expectEqual(CppUtils::String::getNumberLength(10, 2), 4uz); - }); - - suite.addTest("toString", [&] { - suite.expectEqual(CppUtils::String::toString<2>(0), "0"sv); - suite.expectEqual(CppUtils::String::toString<2>(1), "1"sv); - suite.expectEqual(CppUtils::String::toString<2>(2), "10"sv); - suite.expectEqual(CppUtils::String::toString<2>(10), "1010"sv); - - suite.expectEqual(CppUtils::String::toString(0), "0"sv); - suite.expectEqual(CppUtils::String::toString(1), "1"sv); - suite.expectEqual(CppUtils::String::toString(-1), "-1"sv); - - suite.expectEqual(CppUtils::String::toString(42), "42"sv); - suite.expectEqual(CppUtils::String::toString(42u), "42"sv); - suite.expectEqual(CppUtils::String::toString(42uz), "42"sv); - suite.expectEqual(CppUtils::String::toString(42ll), "42"sv); - - suite.expectEqual(CppUtils::String::toString(-42), "-42"sv); - suite.expectEqual(CppUtils::String::toString(-42ll), "-42"sv); - - suite.expectEqual(CppUtils::String::toString(123'456), "123456"sv); - suite.expectEqual(CppUtils::String::toString(-123'456), "-123456"sv); - }); - - suite.addTest("split", [&] { + }; + + suite.test("getNumberLength") = [&] { + expect(CppUtils::String::getNumberLength(0)) == 1uz; + expect(CppUtils::String::getNumberLength(1)) == 1uz; + expect(CppUtils::String::getNumberLength(2)) == 1uz; + expect(CppUtils::String::getNumberLength(10)) == 2uz; + + expect(CppUtils::String::getNumberLength(-1)) == 2uz; + expect(CppUtils::String::getNumberLength(-2)) == 2uz; + expect(CppUtils::String::getNumberLength(-10)) == 3uz; + + expect(CppUtils::String::getNumberLength(0, 2)) == 1uz; + expect(CppUtils::String::getNumberLength(1, 2)) == 1uz; + expect(CppUtils::String::getNumberLength(2, 2)) == 2uz; + expect(CppUtils::String::getNumberLength(10, 2)) == 4uz; + }; + + suite.test("toString") = [&] { + expect(CppUtils::String::toString<2>(0)) == "0"; + expect(CppUtils::String::toString<2>(1)) == "1"; + expect(CppUtils::String::toString<2>(2)) == "10"; + expect(CppUtils::String::toString<2>(10)) == "1010"; + + expect(CppUtils::String::toString(0)) == "0"; + expect(CppUtils::String::toString(1)) == "1"; + expect(CppUtils::String::toString(-1)) == "-1"; + + expect(CppUtils::String::toString(42)) == "42"; + expect(CppUtils::String::toString(42u)) == "42"; + expect(CppUtils::String::toString(42uz)) == "42"; + expect(CppUtils::String::toString(42ll)) == "42"; + + expect(CppUtils::String::toString(-42)) == "-42"; + expect(CppUtils::String::toString(-42ll)) == "-42"; + + expect(CppUtils::String::toString(123'456)) == "123456"; + expect(CppUtils::String::toString(-123'456)) == "-123456"; + }; + + suite.test("split") = [&] { auto splitString = CppUtils::String::split("Hello World!", " "); - suite.expectEqual(std::ranges::size(splitString), 2uz); - suite.expectEqual(splitString[0], "Hello"sv); - suite.expectEqual(splitString[1], "World!"sv); - }); + expect(std::ranges::size(splitString)) == 2uz; + expect(splitString[0]) == "Hello"; + expect(splitString[1]) == "World!"; + }; - suite.addTest("toHexString", [&] { + suite.test("toHexString") = [&] { const auto data = std::array{std::byte{0x00}, std::byte{0x0f}, std::byte{0x10}, std::byte{0xff}}; - suite.expectEqual(CppUtils::String::toHexString(data), "000f10ff"); - suite.expectEqual(CppUtils::String::toHexString("Hello"), "48656c6c6f"); - }); + expect(CppUtils::String::toHexString(data)) == "000f10ff"; + expect(CppUtils::String::toHexString("Hello")) == "48656c6c6f"; + }; - suite.addTest("fromHexString", [&] { + suite.test("fromHexString") = [&] { const auto decoded = CppUtils::String::fromHexString("000f10ff"); const auto expectedData = std::array{std::byte{0x00}, std::byte{0x0f}, std::byte{0x10}, std::byte{0xff}}; - suite.expect(std::ranges::equal(decoded, expectedData)); - }); + expect(std::ranges::equal(decoded, expectedData)); + }; - suite.addTest("toLowercase", [&] { - suite.expectEqual(CppUtils::String::toLowercase('A'), 'a'); - suite.expectEqual(CppUtils::String::toLowercase("HELLO"), "hello"); + suite.test("toLowercase") = [&] { + expect(CppUtils::String::toLowercase('A')) == 'a'; + expect(CppUtils::String::toLowercase("HELLO")) == "hello"; const auto original = std::string{"HELLO"}; - suite.expectEqual(CppUtils::String::toLowercase(original), "hello"); - suite.expectEqual(CppUtils::String::toLowercase(std::string{"HELLO"}), "hello"); - suite.expectEqual(CppUtils::String::toLowercase("HELLO"sv), "hello"); - suite.expectEqual(CppUtils::String::toLowercase(L"HELLO"sv), L"hello"); - }); - - suite.addTest("toUppercase", [&] { - suite.expectEqual(CppUtils::String::toUppercase('a'), 'A'); - suite.expectEqual(CppUtils::String::toUppercase("hello"), "HELLO"); + expect(CppUtils::String::toLowercase(original)) == "hello"; + expect(CppUtils::String::toLowercase(std::string{"HELLO"})) == "hello"; + expect(CppUtils::String::toLowercase("HELLO"sv)) == "hello"; + expect(CppUtils::String::toLowercase(L"HELLO"sv)) == L"hello"; + }; + + suite.test("toUppercase") = [&] { + expect(CppUtils::String::toUppercase('a')) == 'A'; + expect(CppUtils::String::toUppercase("hello")) == "HELLO"; const auto original = std::string{"hello"}; - suite.expectEqual(CppUtils::String::toUppercase(original), "HELLO"); - suite.expectEqual(CppUtils::String::toUppercase(std::string{"hello"}), "HELLO"); - suite.expectEqual(CppUtils::String::toUppercase("hello"sv), "HELLO"); - suite.expectEqual(CppUtils::String::toUppercase(L"hello"sv), L"HELLO"); - }); - }}; + expect(CppUtils::String::toUppercase(original)) == "HELLO"; + expect(CppUtils::String::toUppercase(std::string{"hello"})) == "HELLO"; + expect(CppUtils::String::toUppercase("hello"sv)) == "HELLO"; + expect(CppUtils::String::toUppercase(L"hello"sv)) == L"HELLO"; + }; + + suite.test("formatValue") = [&] { + expect(CppUtils::String::formatValue(42)) == "42"; + expect(CppUtils::String::formatValue("hello")) == "hello"; + expect(CppUtils::String::formatValue(std::byte{0x2A})) == "0x2A"; + + const auto vector = std::vector{1, 2, 3}; + expect(CppUtils::String::formatValue(vector)) == "[1, 2, 3]"; + + const auto stringVector = std::vector{"foo", "bar"}; + expect(CppUtils::String::formatValue(stringVector)) == R"(["foo", "bar"])"; + + const auto pair = std::pair{1, "test"}; + expect(CppUtils::String::formatValue(pair)) == R"((1, "test"))"; + + const auto tuple = std::tuple{1, 2.5, "item"}; + expect(CppUtils::String::formatValue(tuple)) == R"((1, 2.5, "item"))"; + + const auto variant = std::variant{"active"}; + expect(CppUtils::String::formatValue(variant)) == "active"; + + const auto path = std::filesystem::path{"/usr/local/bin"}; + expect(CppUtils::String::formatValue(path)) == "/usr/local/bin"; + + const auto emptyOptional = std::optional{}; + expect(CppUtils::String::formatValue(emptyOptional)) == "nullopt"; + + const auto populatedOptional = std::optional{"hello"}; + expect(CppUtils::String::formatValue(populatedOptional)) == "hello"; + + const auto nullUniquePtr = std::unique_ptr{}; + expect(CppUtils::String::formatValue(nullUniquePtr)) == "nullptr"; + + const auto populatedUniquePtr = std::make_unique(42); + expect(CppUtils::String::formatValue(populatedUniquePtr)) == "42"; + + const auto vectorOfOptionals = std::vector>{1, std::nullopt, 3}; + expect(CppUtils::String::formatValue(vectorOfOptionals)) == "[1, nullopt, 3]"; + }; + }; } diff --git a/tests/System/Error.mpp b/tests/System/Error.mpp index c3a6c452..dc94991c 100644 --- a/tests/System/Error.mpp +++ b/tests/System/Error.mpp @@ -5,51 +5,51 @@ import CppUtils; namespace CppUtils::UnitTest::System::Error { - auto _ = TestSuite{"System/Error", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("throwErrno", [&] { - suite.expectThrow([&] { + auto _ = testSuite("System/Error").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test("throwErrno") = [&] { + expect([&] { CppUtils::System::throwErrno(-1, "Test error"); - }); - suite.expectNoThrow<>([&] { + }) == throws; + expect([&] { CppUtils::System::throwErrno(0, "Test success"); - }); - }); + }) == noThrow; + }; - suite.addTest("expectErrno", [&] { + suite.test("expectErrno") = [&] { { auto result = CppUtils::System::expectErrno(-1, "Test error"); - suite.expect(not result); + expect(not result); } { auto result = CppUtils::System::expectErrno(0, "Test success"); - suite.expect(result); - suite.expectEqual(result.value(), 0); + expect(result); + expect(result.value()) == 0; } - }); + }; #if defined(OS_WINDOWS) - suite.addTest("throwLastError", [&] { - suite.expectThrow([&] { + suite.test("throwLastError") = [&] { + expect([&] { CppUtils::System::throwLastError("Test last error"); - }); - }); + }) == throws; + }; - suite.addTest("throwLastErrorIf", [&] { - suite.expectThrow([&] { + suite.test("throwLastErrorIf") = [&] { + expect([&] { CppUtils::System::throwLastErrorIf(1, "Test last error if"); - }); - suite.expectNoThrow<>([&] { + }) == throws; + expect([&] { CppUtils::System::throwLastErrorIf(0, "Test last error if"); - }); - }); + }) == noThrow; + }; - suite.addTest("expectNoError", [&] { + suite.test("expectNoError") = [&] { auto result = CppUtils::System::expectNoError(0, "Test no error"); - suite.expect(result); - suite.expectEqual(result.value(), 0); + expect(result); + expect(result.value()) == 0; result = CppUtils::System::expectNoError(1, "Test no error"); - suite.expect(not result); - }); + expect(not result); + }; #endif - }}; + }; } diff --git a/tests/System/Pipe.mpp b/tests/System/Pipe.mpp new file mode 100644 index 00000000..0819ec16 --- /dev/null +++ b/tests/System/Pipe.mpp @@ -0,0 +1,46 @@ +export module CppUtils.UnitTests.System.Pipe; + +import std; +import CppUtils; + +namespace CppUtils::UnitTest::System::Pipe +{ + auto _ = testSuite("System/Pipe").dependsOn("UnitTest") = []([[maybe_unused]] TestSuite& suite) { +#if defined(__linux__) + using namespace std::chrono_literals; + + suite.test("PipeChannel basic write and read") = [&] { + auto channel = CppUtils::System::PipeChannel{}; + const auto message = std::string_view{"Hello IPC Channel"}; + expect(channel.write(message)) == std::ranges::size(message); + + auto buffer = std::array{}; + const auto bytesRead = channel.read(std::as_writable_bytes(std::span{buffer})); + expect(bytesRead) == std::ranges::size(message); + expect(std::string_view{std::ranges::data(buffer), bytesRead}) == message; + }; + + suite.test("PipeChannel typed object serialization") = [&] { + auto channel = CppUtils::System::PipeChannel{}; + const auto expectedValue = std::uint64_t{0xDEADBEEFCAFEBABEULL}; + channel.writeObject(expectedValue); + + auto actualValue = std::uint64_t{0}; + const auto success = channel.readObject(actualValue); + expect(success); + expect(actualValue) == expectedValue; + }; + + suite.test("PipeChannel waitForData readiness") = [&] { + auto channel = CppUtils::System::PipeChannel{}; + expect(not channel.waitForData(5ms)); + + channel.write("data"); + expect(channel.waitForData(50ms)); + + channel.closeWrite(); + expect(channel.readAll()) == "data"; + }; +#endif + }; +} diff --git a/tests/System/Process.mpp b/tests/System/Process.mpp new file mode 100644 index 00000000..1e165423 --- /dev/null +++ b/tests/System/Process.mpp @@ -0,0 +1,29 @@ +export module CppUtils.UnitTests.System.Process; + +import std; +import CppUtils; + +namespace CppUtils::UnitTest::System::Process +{ + auto _ = testSuite("System/Process").dependsOn("UnitTest") = []([[maybe_unused]] TestSuite& suite) { +#if defined(__linux__) + using CppUtils::System::Signal; + + suite.test("signalToString symbolic decoding") = [&] { + expect(CppUtils::System::signalToString(Signal::SegmentationFault)) == "SIGSEGV"; + expect(CppUtils::System::signalToString(Signal::Abort)) == "SIGABRT"; + expect(CppUtils::System::signalToString(Signal::Kill)) == "SIGKILL"; + expect(CppUtils::System::signalToString(Signal::Interrupt)) == "SIGINT"; + expect(CppUtils::System::signalToString(Signal::Terminate)) == "SIGTERM"; + expect(CppUtils::System::signalToString(Signal::FloatingPointException)) == "SIGFPE"; + expect(CppUtils::System::signalToString(static_cast(9999))) == "Unknown"; + }; + + suite.test("signalToExplanation diagnostics") = [&] { + expect(CppUtils::System::signalToExplanation(Signal::SegmentationFault).contains("Segmentation fault")); + expect(CppUtils::System::signalToExplanation(Signal::Abort).contains("Abnormal termination")); + expect(CppUtils::System::signalToExplanation(Signal::FloatingPointException).contains("Arithmetic exception")); + }; +#endif + }; +} diff --git a/tests/System/Sandbox.mpp b/tests/System/Sandbox.mpp new file mode 100644 index 00000000..c1233b33 --- /dev/null +++ b/tests/System/Sandbox.mpp @@ -0,0 +1,73 @@ +export module CppUtils.UnitTests.System.Sandbox; + +import std; +import CppUtils; + +namespace CppUtils::UnitTest::System::Sandbox +{ + auto _ = testSuite("System/Sandbox").dependsOn("UnitTest") = []([[maybe_unused]] TestSuite& suite) { +#if defined(__linux__) + using namespace std::chrono_literals; + + suite.test("runIsolated normal execution") = [&] { + const auto result = CppUtils::System::runIsolated([] { + // Routine executing normally + }); + + expect(result.has_value()); + expect(result->status == CppUtils::System::ExecutionStatus::Success); + }; + + suite.test("runIsolated exception capture") = [&] { + const auto result = CppUtils::System::runIsolated([] { + throw std::runtime_error{"Isolated custom exception"}; + }); + + expect(result.has_value()); + expect(result->status == CppUtils::System::ExecutionStatus::Failure); + expect(result->payload.contains("Isolated custom exception")); + }; + + suite.test("runIsolated crash capture on SIGSEGV") = [&] { + const auto result = CppUtils::System::runIsolated([] { +# if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wnull-dereference" +# endif + volatile int* nullPointer = nullptr; + *nullPointer = 42; +# if defined(__clang__) +# pragma clang diagnostic pop +# endif + }); + + expect(not result.has_value()); + expect(result.error().crash.has_value()); + expect(result.error().crash->signal) == CppUtils::System::Signal::SegmentationFault; + expect(result.error().crash->signalName()) == "SIGSEGV"; + }; + + suite.test("runIsolated crash capture on SIGABRT") = [&] { + const auto result = CppUtils::System::runIsolated([] { + std::abort(); + }); + + expect(not result.has_value()); + expect(result.error().crash.has_value()); + expect(result.error().crash->signal) == CppUtils::System::Signal::Abort; + expect(result.error().crash->signalName()) == "SIGABRT"; + }; + + suite.test("runIsolated timeout handling") = [&] { + const auto result = CppUtils::System::runIsolated([] { + std::this_thread::sleep_for(1s); + }, 50ms); + + expect(not result.has_value()); + expect(result.error().crash.has_value()); + expect(result.error().crash->signal) == CppUtils::System::Signal::Kill; + expect(result.error().crash->timedOut); + }; +#endif + }; +} diff --git a/tests/Terminal/AutoScroll.mpp b/tests/Terminal/AutoScroll.mpp index 4f61ffbc..1a0d2b98 100644 --- a/tests/Terminal/AutoScroll.mpp +++ b/tests/Terminal/AutoScroll.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::AutoScroll { - auto _ = TestSuite{"Terminal/AutoScroll", {"Terminal/Canvas", "Terminal/Text"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal/AutoScroll").dependsOn("Terminal/Canvas", "Terminal/Text") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Basic AutoScroll", [&] { + suite.test("Basic AutoScroll") = [&] { const auto size = CppUtils::Container::Size2{10, 1}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -18,7 +18,7 @@ namespace CppUtils::UnitTest::Terminal::AutoScroll canvas.print(); { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'H'); + expect(accessor->getChar({0, 0}).character) == U'H'; } std::this_thread::sleep_for(200ms); @@ -26,12 +26,12 @@ namespace CppUtils::UnitTest::Terminal::AutoScroll canvas.print(); { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'W'); + expect(accessor->getChar({0, 0}).character) == U'W'; } std::this_thread::sleep_for(200ms); - }); + }; - suite.addTest("AutoScroll on Hover", [&] { + suite.test("AutoScroll on Hover") = [&] { const auto canvasSize = CppUtils::Container::Size2{20, 5}; auto canvas = CppUtils::Terminal::Canvas{canvasSize}; @@ -43,23 +43,23 @@ namespace CppUtils::UnitTest::Terminal::AutoScroll .setScrollOnHover(true); canvas.print(); - suite.expectEqual(autoScroll.isScrollOnHover(), true); - suite.expectEqual(autoScroll.isAutoScrolling(), false); + expect(autoScroll.isScrollOnHover()); + not expect(autoScroll.isAutoScrolling()); const auto canvasOrigin = canvas.getAbsolutePosition(); auto hoverEvent = CppUtils::Terminal::Mouse::Event{}; hoverEvent.absolutePosition = canvasOrigin + CppUtils::Container::Size2{2uz, 0uz}; canvas.onMouseEvent(hoverEvent); - suite.expectEqual(autoScroll.isAutoScrolling(), true); - suite.expect(CppUtils::Thread::pollFor([&] { return autoScroll.getScrollOffset() > 0uz; }, 2s, 10ms)); + expect(autoScroll.isAutoScrolling()); + expect(CppUtils::Thread::pollFor([&] { return autoScroll.getScrollOffset() > 0uz; }, 2s, 10ms)); auto leaveEvent = CppUtils::Terminal::Mouse::Event{}; leaveEvent.absolutePosition = canvasOrigin + CppUtils::Container::Size2{15uz, 0uz}; canvas.onMouseEvent(leaveEvent); - suite.expectEqual(autoScroll.isAutoScrolling(), false); - suite.expectEqual(autoScroll.getScrollOffset(), 0uz); - }); - }}; + not expect(autoScroll.isAutoScrolling()); + expect(autoScroll.getScrollOffset()) == 0uz; + }; + }; } diff --git a/tests/Terminal/Button.mpp b/tests/Terminal/Button.mpp index 408f2219..efc5ead7 100644 --- a/tests/Terminal/Button.mpp +++ b/tests/Terminal/Button.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::Button { - auto _ = TestSuite{"Terminal/Button", {"Terminal/Canvas", "Terminal/Text", "Terminal/Element"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal/Button").dependsOn("Terminal/Canvas", "Terminal/Text", "Terminal/Element") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Click", [&] { + suite.test("Click") = [&] { const auto size = CppUtils::Container::Size2{10, 3}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -20,7 +20,7 @@ namespace CppUtils::UnitTest::Terminal::Button canvas.print(); std::this_thread::sleep_for(200ms); - suite.expectEqual(clicked, false); + not expect(clicked); auto hoverEvent = CppUtils::Terminal::Mouse::Event{ .button = CppUtils::Terminal::Mouse::Button::None, @@ -33,7 +33,7 @@ namespace CppUtils::UnitTest::Terminal::Button { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({5, 1}).attributes.backgroundColor, CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Cyan); + expect(accessor->getChar({5, 1}).attributes.backgroundColor) == CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Cyan; } auto pressEvent = hoverEvent; @@ -45,9 +45,9 @@ namespace CppUtils::UnitTest::Terminal::Button { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({5, 1}).attributes.backgroundColor, CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Black); + expect(accessor->getChar({5, 1}).attributes.backgroundColor) == CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Black; } - suite.expectEqual(clicked, false); + not expect(clicked); auto releaseEvent = pressEvent; releaseEvent.pressed = false; @@ -55,14 +55,14 @@ namespace CppUtils::UnitTest::Terminal::Button canvas.print(); std::this_thread::sleep_for(200ms); - suite.expectEqual(clicked, true); + expect(clicked); { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({5, 1}).attributes.backgroundColor, CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Cyan); + expect(accessor->getChar({5, 1}).attributes.backgroundColor) == CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Cyan; } - }); + }; - suite.addTest("CustomStyle", [&] { + suite.test("CustomStyle") = [&] { const auto size = CppUtils::Container::Size2{20, 3}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -77,11 +77,11 @@ namespace CppUtils::UnitTest::Terminal::Button { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).attributes.backgroundColor, CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Red); + expect(accessor->getChar({0, 0}).attributes.backgroundColor) == CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Red; } - }); + }; - suite.addTest("CustomBorders", [&] { + suite.test("CustomBorders") = [&] { const auto size = CppUtils::Container::Size2{20, 3}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -97,16 +97,16 @@ namespace CppUtils::UnitTest::Terminal::Button { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'┌'); - suite.expectEqual(accessor->getChar({19, 0}).character, U'┐'); - suite.expectEqual(accessor->getChar({0, 2}).character, U'└'); - suite.expectEqual(accessor->getChar({19, 2}).character, U'┘'); - suite.expectEqual(accessor->getChar({1, 0}).character, U'─'); - suite.expectEqual(accessor->getChar({0, 1}).character, U'│'); + expect(accessor->getChar({0, 0}).character) == U'┌'; + expect(accessor->getChar({19, 0}).character) == U'┐'; + expect(accessor->getChar({0, 2}).character) == U'└'; + expect(accessor->getChar({19, 2}).character) == U'┘'; + expect(accessor->getChar({1, 0}).character) == U'─'; + expect(accessor->getChar({0, 1}).character) == U'│'; } - }); + }; - suite.addTest("TextAlignment", [&] { + suite.test("TextAlignment") = [&] { const auto size = CppUtils::Container::Size2{20, 1}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -119,7 +119,7 @@ namespace CppUtils::UnitTest::Terminal::Button std::this_thread::sleep_for(200ms); { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'C'); + expect(accessor->getChar({0, 0}).character) == U'C'; } textRef.setHorizontalAlignment(CppUtils::Terminal::Alignment::Center); @@ -127,7 +127,7 @@ namespace CppUtils::UnitTest::Terminal::Button std::this_thread::sleep_for(200ms); { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({7, 0}).character, U'C'); + expect(accessor->getChar({7, 0}).character) == U'C'; } textRef.setHorizontalAlignment(CppUtils::Terminal::Alignment::End); @@ -135,11 +135,11 @@ namespace CppUtils::UnitTest::Terminal::Button std::this_thread::sleep_for(200ms); { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({15, 0}).character, U'C'); + expect(accessor->getChar({15, 0}).character) == U'C'; } - }); + }; - suite.addTest("TextOverflow", [&] { + suite.test("TextOverflow") = [&] { const auto size = CppUtils::Container::Size2{20, 1}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -151,8 +151,8 @@ namespace CppUtils::UnitTest::Terminal::Button { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({17, 0}).character, U'…'); + expect(accessor->getChar({17, 0}).character) == U'…'; } - }); - }}; + }; + }; } diff --git a/tests/Terminal/Canvas.mpp b/tests/Terminal/Canvas.mpp index 7e988a1c..32522139 100644 --- a/tests/Terminal/Canvas.mpp +++ b/tests/Terminal/Canvas.mpp @@ -5,29 +5,29 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::Canvas { - auto _ = TestSuite{"Terminal/Canvas", {"UnitTest", "Thread/Scheduler", "String/Encoding"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal/Canvas").dependsOn("UnitTest", "Thread/Scheduler", "String/Encoding") = [](TestSuite& suite) { using namespace std::literals; using namespace std::chrono_literals; - suite.addTest("Canvas with 0x0 size", [&] { + suite.test("Canvas with 0x0 size") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{0, 0}}; canvas.getWritableView().fill('.'); canvas.print(); - }); + }; - suite.addTest("Canvas with 0 height", [&] { + suite.test("Canvas with 0 height") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{10, 0}}; canvas.getWritableView().fill('.'); canvas.print(); - }); + }; - suite.addTest("Canvas with 0 width", [&] { + suite.test("Canvas with 0 width") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{0, 10}}; canvas.getWritableView().fill('.'); canvas.print(); - }); + }; - suite.addTest("Size", [&] { + suite.test("Size") = [&] { { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{10, 5}}; canvas.getWritableView().fill('/'); @@ -46,9 +46,9 @@ namespace CppUtils::UnitTest::Terminal::Canvas canvas.print(); std::this_thread::sleep_for(300ms); } - }); + }; - suite.addTest("Fill", [&] { + suite.test("Fill") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{10, 5}}; const auto chars = "-\\|/"sv; for (auto i = 0uz; i < 5; ++i) @@ -58,9 +58,9 @@ namespace CppUtils::UnitTest::Terminal::Canvas canvas.getWritableView().fill(c); canvas.print(); } - }); + }; - suite.addTest("Spinner", [&] { + suite.test("Spinner") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{1, 1}}; const auto frames = std::array{ CppUtils::Terminal::FixedAreaBuffer<1, 1>{'-'}, @@ -73,9 +73,9 @@ namespace CppUtils::UnitTest::Terminal::Canvas canvas.close(); }, 1s); canvas.wait(); - }); + }; - suite.addTest("Print text", [&] { + suite.test("Print text") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{10, 5}}; { auto view = canvas.getWritableView(); @@ -84,10 +84,10 @@ namespace CppUtils::UnitTest::Terminal::Canvas } canvas.print(); - suite.expectEqual(canvas.toString(), "..........\n.Hello Wor\nld!.......\n..........\n..........\n"sv); - }); + expect(canvas.toString()) == "..........\n.Hello Wor\nld!.......\n..........\n..........\n"; + }; - suite.addTest("Print Unicode", [&] { + suite.test("Print Unicode") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{15, 5}}; { auto view = canvas.getWritableView(); @@ -100,10 +100,10 @@ namespace CppUtils::UnitTest::Terminal::Canvas } canvas.print(); - suite.expectEqual(canvas.toString(), "😊🤖🐸🐊🏊-----\n-🤖------------\n------🐸-------\n------------🐊-\n-------------🏊\n"sv); - }); + expect(canvas.toString()) == "😊🤖🐸🐊🏊-----\n-🤖------------\n------🐸-------\n------------🐊-\n-------------🏊\n"; + }; - suite.addTest("Bouncing ball", [&] { + suite.test("Bouncing ball") = [&] { const auto terminalSize = CppUtils::Terminal::getTerminalSize(); auto canvasSize = terminalSize; canvasSize.height() = std::ranges::min(canvasSize.width() * 1 / 3, terminalSize.height() - 2); @@ -146,6 +146,6 @@ namespace CppUtils::UnitTest::Terminal::Canvas canvas.print(); std::this_thread::sleep_for(5ms); } - }); - }}; + }; + }; } diff --git a/tests/Terminal/Element.mpp b/tests/Terminal/Element.mpp index 35698ae2..691f4bfc 100644 --- a/tests/Terminal/Element.mpp +++ b/tests/Terminal/Element.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::Element { - auto _ = TestSuite{"Terminal/Element", {"Terminal/Canvas"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal/Element").dependsOn("Terminal/Canvas") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Borders", [&] { + suite.test("Borders") = [&] { const auto size = CppUtils::Container::Size2{10, 3}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -19,16 +19,16 @@ namespace CppUtils::UnitTest::Terminal::Element { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'┌'); - suite.expectEqual(accessor->getChar({9, 0}).character, U'┐'); - suite.expectEqual(accessor->getChar({0, 2}).character, U'└'); - suite.expectEqual(accessor->getChar({9, 2}).character, U'┘'); - suite.expectEqual(accessor->getChar({1, 0}).character, U'─'); - suite.expectEqual(accessor->getChar({0, 1}).character, U'│'); + expect(accessor->getChar({0, 0}).character) == U'┌'; + expect(accessor->getChar({9, 0}).character) == U'┐'; + expect(accessor->getChar({0, 2}).character) == U'└'; + expect(accessor->getChar({9, 2}).character) == U'┘'; + expect(accessor->getChar({1, 0}).character) == U'─'; + expect(accessor->getChar({0, 1}).character) == U'│'; } - }); + }; - suite.addTest("Padding", [&] { + suite.test("Padding") = [&] { struct ChildWidget final: public CppUtils::Terminal::Element { explicit inline ChildWidget(const CppUtils::Container::Size2<>& size): @@ -55,9 +55,9 @@ namespace CppUtils::UnitTest::Terminal::Element { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'┌'); - suite.expectEqual(accessor->getChar({2, 2}).character, U'A'); + expect(accessor->getChar({0, 0}).character) == U'┌'; + expect(accessor->getChar({2, 2}).character) == U'A'; } - }); - }}; + }; + }; } diff --git a/tests/Terminal/Layout.mpp b/tests/Terminal/Layout.mpp index 2fae48a7..c3e74322 100644 --- a/tests/Terminal/Layout.mpp +++ b/tests/Terminal/Layout.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::Layout { - auto _ = TestSuite{"Terminal/Layout", {"Terminal/Canvas", "Terminal/Text"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal/Layout").dependsOn("Terminal/Canvas", "Terminal/Text") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Column", [&] { + suite.test("Column") = [&] { const auto size = CppUtils::Container::Size2{10, 4}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -19,13 +19,13 @@ namespace CppUtils::UnitTest::Terminal::Layout canvas.print(); const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'L'); - suite.expectEqual(accessor->getChar({5, 0}).character, U'1'); - suite.expectEqual(accessor->getChar({0, 1}).character, U'L'); - suite.expectEqual(accessor->getChar({5, 1}).character, U'2'); - }); + expect(accessor->getChar({0, 0}).character) == U'L'; + expect(accessor->getChar({5, 0}).character) == U'1'; + expect(accessor->getChar({0, 1}).character) == U'L'; + expect(accessor->getChar({5, 1}).character) == U'2'; + }; - suite.addTest("Row JustifyCenter", [&] { + suite.test("Row JustifyCenter") = [&] { const auto size = CppUtils::Container::Size2{20, 1}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -39,12 +39,12 @@ namespace CppUtils::UnitTest::Terminal::Layout { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({8, 0}).character, U'I'); - suite.expectEqual(accessor->getChar({11, 0}).character, U'm'); + expect(accessor->getChar({8, 0}).character) == U'I'; + expect(accessor->getChar({11, 0}).character) == U'm'; } - }); + }; - suite.addTest("AbsolutePosition & ZIndex", [&] { + suite.test("AbsolutePosition & ZIndex") = [&] { const auto size = CppUtils::Container::Size2{10, 2}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -64,7 +64,7 @@ namespace CppUtils::UnitTest::Terminal::Layout { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'B'); + expect(accessor->getChar({0, 0}).character) == U'B'; } std::this_thread::sleep_for(200ms); @@ -72,12 +72,12 @@ namespace CppUtils::UnitTest::Terminal::Layout canvas.print(); { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'A'); + expect(accessor->getChar({0, 0}).character) == U'A'; } std::this_thread::sleep_for(200ms); - }); + }; - suite.addTest("Absolute right bottom positioning", [&] { + suite.test("Absolute right bottom positioning") = [&] { const auto size = CppUtils::Container::Size2{10, 3}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -92,9 +92,9 @@ namespace CppUtils::UnitTest::Terminal::Layout { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({6, 2}).character, U'E'); - suite.expectEqual(accessor->getChar({8, 2}).character, U'd'); + expect(accessor->getChar({6, 2}).character) == U'E'; + expect(accessor->getChar({8, 2}).character) == U'd'; } - }); - }}; + }; + }; } diff --git a/tests/Terminal/Mouse.mpp b/tests/Terminal/Mouse.mpp index 8e45720e..78ef9e90 100644 --- a/tests/Terminal/Mouse.mpp +++ b/tests/Terminal/Mouse.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::Mouse { - auto _ = TestSuite{"Terminal/Mouse", {"UnitTest", "Terminal/Canvas"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal/Mouse").dependsOn("UnitTest", "Terminal/Canvas") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Tracking", [] { + suite.test("Tracking") = [] { const auto size = CppUtils::Container::Size2{40, 20}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -70,6 +70,6 @@ namespace CppUtils::UnitTest::Terminal::Mouse if (needPrint) canvas.print(); } - }); - }}; + }; + }; } diff --git a/tests/Terminal/Primitive.mpp b/tests/Terminal/Primitive.mpp index d9384efd..94af6d94 100644 --- a/tests/Terminal/Primitive.mpp +++ b/tests/Terminal/Primitive.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::Primitive { - auto _ = TestSuite{"Terminal/Primitive", {"UnitTest", "Terminal/Canvas"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal/Primitive").dependsOn("UnitTest", "Terminal/Canvas") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Draw Line", [&] { + suite.test("Draw Line") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{10, 10}}; { auto view = canvas.getWritableView(); @@ -20,13 +20,13 @@ namespace CppUtils::UnitTest::Terminal::Primitive { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(static_cast(accessor->getChar({5, 0}).character), static_cast(U'-')); - suite.expectEqual(static_cast(accessor->getChar({0, 5}).character), static_cast(U'|')); - suite.expectEqual(static_cast(accessor->getChar({5, 5}).character), static_cast(U'\\')); + expect(static_cast(accessor->getChar({5, 0}).character)) == static_cast(U'-'); + expect(static_cast(accessor->getChar({0, 5}).character)) == static_cast(U'|'); + expect(static_cast(accessor->getChar({5, 5}).character)) == static_cast(U'\\'); } - }); + }; - suite.addTest("Draw Rectangle Outline", [&] { + suite.test("Draw Rectangle Outline") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{20, 10}}; { auto view = canvas.getWritableView(); @@ -36,17 +36,17 @@ namespace CppUtils::UnitTest::Terminal::Primitive { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({1, 1}).character, U'#'); - suite.expectEqual(accessor->getChar({18, 1}).character, U'#'); - suite.expectEqual(accessor->getChar({1, 8}).character, U'#'); - suite.expectEqual(accessor->getChar({18, 8}).character, U'#'); + expect(accessor->getChar({1, 1}).character) == U'#'; + expect(accessor->getChar({18, 1}).character) == U'#'; + expect(accessor->getChar({1, 8}).character) == U'#'; + expect(accessor->getChar({18, 8}).character) == U'#'; - suite.expectEqual(accessor->getChar({10, 1}).character, U'#'); - suite.expectEqual(accessor->getChar({10, 5}).character, U' '); + expect(accessor->getChar({10, 1}).character) == U'#'; + expect(accessor->getChar({10, 5}).character) == U' '; } - }); + }; - suite.addTest("Draw Rectangle Filled", [&] { + suite.test("Draw Rectangle Filled") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{20, 10}}; { auto view = canvas.getWritableView(); @@ -56,19 +56,19 @@ namespace CppUtils::UnitTest::Terminal::Primitive { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({10, 0}).character, U' '); + expect(accessor->getChar({10, 0}).character) == U' '; for (auto y = 1uz; y <= 8uz; ++y) - suite.expectEqual(accessor->getChar({10, y}).character, U'#'); - suite.expectEqual(accessor->getChar({10, 9}).character, U' '); + expect(accessor->getChar({10, y}).character) == U'#'; + expect(accessor->getChar({10, 9}).character) == U' '; - suite.expectEqual(accessor->getChar({0, 5}).character, U' '); + expect(accessor->getChar({0, 5}).character) == U' '; for (auto x = 1uz; x <= 18uz; ++x) - suite.expectEqual(accessor->getChar({x, 5}).character, U'#'); - suite.expectEqual(accessor->getChar({19, 5}).character, U' '); + expect(accessor->getChar({x, 5}).character) == U'#'; + expect(accessor->getChar({19, 5}).character) == U' '; } - }); + }; - suite.addTest("Draw Circle Outline", [&] { + suite.test("Draw Circle Outline") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{11, 11}}; { auto view = canvas.getWritableView(); @@ -78,26 +78,26 @@ namespace CppUtils::UnitTest::Terminal::Primitive { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({5, 0}).character, U' '); - suite.expectEqual(accessor->getChar({5, 1}).character, U'O'); + expect(accessor->getChar({5, 0}).character) == U' '; + expect(accessor->getChar({5, 1}).character) == U'O'; for (auto y = 2uz; y <= 8uz; ++y) - suite.expectEqual(accessor->getChar({5, y}).character, U' '); - suite.expectEqual(accessor->getChar({5, 9}).character, U'O'); - suite.expectEqual(accessor->getChar({5, 10}).character, U' '); + expect(accessor->getChar({5, y}).character) == U' '; + expect(accessor->getChar({5, 9}).character) == U'O'; + expect(accessor->getChar({5, 10}).character) == U' '; - suite.expectEqual(accessor->getChar({0, 5}).character, U' '); - suite.expectEqual(accessor->getChar({1, 5}).character, U'O'); + expect(accessor->getChar({0, 5}).character) == U' '; + expect(accessor->getChar({1, 5}).character) == U'O'; for (auto x = 2uz; x <= 8uz; ++x) - suite.expectEqual(accessor->getChar({x, 5}).character, U' '); - suite.expectEqual(accessor->getChar({9, 5}).character, U'O'); - suite.expectEqual(accessor->getChar({10, 5}).character, U' '); + expect(accessor->getChar({x, 5}).character) == U' '; + expect(accessor->getChar({9, 5}).character) == U'O'; + expect(accessor->getChar({10, 5}).character) == U' '; - suite.expectEqual(accessor->getChar({0, 0}).character, U' '); - suite.expectEqual(accessor->getChar({10, 10}).character, U' '); + expect(accessor->getChar({0, 0}).character) == U' '; + expect(accessor->getChar({10, 10}).character) == U' '; } - }); + }; - suite.addTest("Draw Circle Filled", [&] { + suite.test("Draw Circle Filled") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{11, 11}}; { auto view = canvas.getWritableView(); @@ -107,19 +107,19 @@ namespace CppUtils::UnitTest::Terminal::Primitive { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({5, 0}).character, U' '); + expect(accessor->getChar({5, 0}).character) == U' '; for (auto y = 1uz; y <= 9uz; ++y) - suite.expectEqual(accessor->getChar({5, y}).character, U'#'); - suite.expectEqual(accessor->getChar({5, 10}).character, U' '); + expect(accessor->getChar({5, y}).character) == U'#'; + expect(accessor->getChar({5, 10}).character) == U' '; - suite.expectEqual(accessor->getChar({0, 5}).character, U' '); + expect(accessor->getChar({0, 5}).character) == U' '; for (auto x = 1uz; x <= 9uz; ++x) - suite.expectEqual(accessor->getChar({x, 5}).character, U'#'); - suite.expectEqual(accessor->getChar({10, 5}).character, U' '); + expect(accessor->getChar({x, 5}).character) == U'#'; + expect(accessor->getChar({10, 5}).character) == U' '; } - }); + }; - suite.addTest("Draw Ellipse Outline", [&] { + suite.test("Draw Ellipse Outline") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{20, 10}}; { auto view = canvas.getWritableView(); @@ -129,23 +129,23 @@ namespace CppUtils::UnitTest::Terminal::Primitive { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({10, 0}).character, U' '); - suite.expectEqual(accessor->getChar({10, 1}).character, U'O'); + expect(accessor->getChar({10, 0}).character) == U' '; + expect(accessor->getChar({10, 1}).character) == U'O'; for (auto y = 2uz; y <= 7uz; ++y) - suite.expectEqual(accessor->getChar({10, y}).character, U' '); - suite.expectEqual(accessor->getChar({10, 8}).character, U'O'); - suite.expectEqual(accessor->getChar({10, 9}).character, U' '); + expect(accessor->getChar({10, y}).character) == U' '; + expect(accessor->getChar({10, 8}).character) == U'O'; + expect(accessor->getChar({10, 9}).character) == U' '; - suite.expectEqual(accessor->getChar({0, 5}).character, U' '); - suite.expectEqual(accessor->getChar({1, 5}).character, U'O'); + expect(accessor->getChar({0, 5}).character) == U' '; + expect(accessor->getChar({1, 5}).character) == U'O'; for (auto x = 2uz; x <= 17uz; ++x) - suite.expectEqual(accessor->getChar({x, 5}).character, U' '); - suite.expectEqual(accessor->getChar({18, 5}).character, U'O'); - suite.expectEqual(accessor->getChar({19, 5}).character, U' '); + expect(accessor->getChar({x, 5}).character) == U' '; + expect(accessor->getChar({18, 5}).character) == U'O'; + expect(accessor->getChar({19, 5}).character) == U' '; } - }); + }; - suite.addTest("Draw Ellipse Filled", [&] { + suite.test("Draw Ellipse Filled") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{20, 10}}; { auto view = canvas.getWritableView(); @@ -155,16 +155,16 @@ namespace CppUtils::UnitTest::Terminal::Primitive { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({10, 0}).character, U' '); + expect(accessor->getChar({10, 0}).character) == U' '; for (auto y = 1uz; y <= 8uz; ++y) - suite.expectEqual(accessor->getChar({10, y}).character, U'#'); - suite.expectEqual(accessor->getChar({10, 9}).character, U' '); + expect(accessor->getChar({10, y}).character) == U'#'; + expect(accessor->getChar({10, 9}).character) == U' '; - suite.expectEqual(accessor->getChar({0, 5}).character, U' '); + expect(accessor->getChar({0, 5}).character) == U' '; for (auto x = 1uz; x <= 18uz; ++x) - suite.expectEqual(accessor->getChar({x, 5}).character, U'#'); - suite.expectEqual(accessor->getChar({19, 5}).character, U' '); + expect(accessor->getChar({x, 5}).character) == U'#'; + expect(accessor->getChar({19, 5}).character) == U' '; } - }); - }}; + }; + }; } diff --git a/tests/Terminal/ProgressBar.mpp b/tests/Terminal/ProgressBar.mpp index b978d190..7ecc6b86 100644 --- a/tests/Terminal/ProgressBar.mpp +++ b/tests/Terminal/ProgressBar.mpp @@ -5,11 +5,11 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::ProgressBar { - auto _ = TestSuite{"Terminal/ProgressBar", {"UnitTest", "Thread/Scheduler", "Terminal/Canvas"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal/ProgressBar").dependsOn("UnitTest", "Thread/Scheduler", "Terminal/Canvas") = [](TestSuite& suite) { using namespace std::literals; using namespace std::chrono_literals; - suite.addTest("Basic", [&] { + suite.test("Basic") = [&] { const auto terminalWidth = CppUtils::Terminal::getTerminalSize().width(); auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{terminalWidth, 1}}; auto& progressBar = canvas.newWidget("Progress bar"); @@ -26,9 +26,9 @@ namespace CppUtils::UnitTest::Terminal::ProgressBar } canvas.wait(); - }); + }; - suite.addTest("Multiple", [&] { + suite.test("Multiple") = [&] { const auto terminalWidth = CppUtils::Terminal::getTerminalSize().width(); auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{terminalWidth, 3}}; auto& layout = canvas.newWidget(CppUtils::Container::Size2{terminalWidth, 3}); @@ -51,9 +51,9 @@ namespace CppUtils::UnitTest::Terminal::ProgressBar } canvas.wait(); - }); + }; - suite.addTest("Styles", [&] { + suite.test("Styles") = [&] { auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{60, 10}}; auto styles = std::vector>{ @@ -81,6 +81,6 @@ namespace CppUtils::UnitTest::Terminal::ProgressBar canvas.print(); std::this_thread::sleep_for(5ms); } - }); - }}; + }; + }; } diff --git a/tests/Terminal/RawTerminal.mpp b/tests/Terminal/RawTerminal.mpp index 35e14211..a3a8244c 100644 --- a/tests/Terminal/RawTerminal.mpp +++ b/tests/Terminal/RawTerminal.mpp @@ -5,88 +5,88 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::RawTerminal { - auto _ = TestSuite{"Terminal/RawTerminal", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("Single RawTerminal", [&] { + auto _ = testSuite("Terminal/RawTerminal").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test("Single RawTerminal") = [] { const auto initialRawState = CppUtils::Terminal::isRawMode(); { auto raw = CppUtils::Terminal::RawTerminal{}; - suite.skipIf(not raw.isInitialized(), "Terminal environment non-interactive"); - suite.expect(CppUtils::Terminal::isRawMode()); + skipIf(not raw.isInitialized(), "Terminal environment non-interactive"); + expect(CppUtils::Terminal::isRawMode()); } - suite.expectEqual(CppUtils::Terminal::isRawMode(), initialRawState); - }); + expect(CppUtils::Terminal::isRawMode()) == initialRawState; + }; - suite.addTest("Move construction", [&] { + suite.test("Move construction") = [] { const auto initialRawState = CppUtils::Terminal::isRawMode(); { auto raw1 = CppUtils::Terminal::RawTerminal{}; - suite.skipIf(not raw1.isInitialized(), "Terminal environment non-interactive"); - suite.expect(CppUtils::Terminal::isRawMode()); + skipIf(not raw1.isInitialized(), "Terminal environment non-interactive"); + expect(CppUtils::Terminal::isRawMode()); { auto raw2 = std::move(raw1); - suite.expect(not raw1.isInitialized()); - suite.expect(raw2.isInitialized()); - suite.expect(CppUtils::Terminal::isRawMode()); + expect(not raw1.isInitialized()); + expect(raw2.isInitialized()); + expect(CppUtils::Terminal::isRawMode()); } - suite.expectEqual(CppUtils::Terminal::isRawMode(), initialRawState); + expect(CppUtils::Terminal::isRawMode()) == initialRawState; } - suite.expectEqual(CppUtils::Terminal::isRawMode(), initialRawState); - }); + expect(CppUtils::Terminal::isRawMode()) == initialRawState; + }; - suite.addTest("Move assignment (raw2 = move(raw1))", [&] { + suite.test("Move assignment (raw2 = move(raw1))") = [] { const auto initialRawState = CppUtils::Terminal::isRawMode(); { auto raw1 = CppUtils::Terminal::RawTerminal{}; - suite.skipIf(not raw1.isInitialized(), "Terminal environment non-interactive"); - suite.expect(CppUtils::Terminal::isRawMode()); + skipIf(not raw1.isInitialized(), "Terminal environment non-interactive"); + expect(CppUtils::Terminal::isRawMode()); auto raw2 = CppUtils::Terminal::RawTerminal{}; - suite.expect(raw2.isInitialized()); - suite.expect(CppUtils::Terminal::isRawMode()); + expect(raw2.isInitialized()); + expect(CppUtils::Terminal::isRawMode()); raw2 = std::move(raw1); - suite.expect(not raw1.isInitialized()); - suite.expect(raw2.isInitialized()); + expect(not raw1.isInitialized()); + expect(raw2.isInitialized()); - suite.expect(CppUtils::Terminal::isRawMode()); + expect(CppUtils::Terminal::isRawMode()); } - suite.expectEqual(CppUtils::Terminal::isRawMode(), initialRawState); - }); + expect(CppUtils::Terminal::isRawMode()) == initialRawState; + }; - suite.addTest("Move assignment (raw1 = move(raw2))", [&] { + suite.test("Move assignment (raw1 = move(raw2))") = [] { const auto initialRawState = CppUtils::Terminal::isRawMode(); { auto raw1 = CppUtils::Terminal::RawTerminal{}; - suite.skipIf(not raw1.isInitialized(), "Terminal environment non-interactive"); - suite.expect(CppUtils::Terminal::isRawMode()); + skipIf(not raw1.isInitialized(), "Terminal environment non-interactive"); + expect(CppUtils::Terminal::isRawMode()); auto raw2 = CppUtils::Terminal::RawTerminal{}; - suite.expect(raw2.isInitialized()); - suite.expect(CppUtils::Terminal::isRawMode()); + expect(raw2.isInitialized()); + expect(CppUtils::Terminal::isRawMode()); raw1 = std::move(raw2); - suite.expect(not raw2.isInitialized()); - suite.expect(raw1.isInitialized()); + expect(not raw2.isInitialized()); + expect(raw1.isInitialized()); - suite.expect(CppUtils::Terminal::isRawMode()); + expect(CppUtils::Terminal::isRawMode()); } - suite.expectEqual(CppUtils::Terminal::isRawMode(), initialRawState); - }); + expect(CppUtils::Terminal::isRawMode()) == initialRawState; + }; - suite.addTest("Multiple handles", [&] { + suite.test("Multiple handles") = [] { const auto defaultRawState = CppUtils::Terminal::isRawMode(); { auto rawDefault = CppUtils::Terminal::RawTerminal{}; - suite.skipIf(not rawDefault.isInitialized(), "Terminal environment non-interactive"); - suite.expect(CppUtils::Terminal::isRawMode()); + skipIf(not rawDefault.isInitialized(), "Terminal environment non-interactive"); + expect(CppUtils::Terminal::isRawMode()); #if defined(OS_LINUX) or defined(OS_MACOS) auto rawStderr = CppUtils::Terminal::RawTerminal{STDERR_FILENO}; - suite.skipIf(not rawStderr.isInitialized(), "Terminal stderr non-interactive"); - suite.expect(CppUtils::Terminal::isRawMode(STDERR_FILENO)); + skipIf(not rawStderr.isInitialized(), "Terminal stderr non-interactive"); + expect(CppUtils::Terminal::isRawMode(STDERR_FILENO)); #endif } - suite.expectEqual(CppUtils::Terminal::isRawMode(), defaultRawState); - }); - }}; + expect(CppUtils::Terminal::isRawMode()) == defaultRawState; + }; + }; } diff --git a/tests/Terminal/Scrollable.mpp b/tests/Terminal/Scrollable.mpp index 197d2d76..2cb67f24 100644 --- a/tests/Terminal/Scrollable.mpp +++ b/tests/Terminal/Scrollable.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::Scrollable { - auto _ = TestSuite{"Terminal/Scrollable", {"Terminal/Canvas", "Terminal/Primitive"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal/Scrollable").dependsOn("Terminal/Canvas", "Terminal/Primitive") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("ScrollableCircle", [&] { + suite.test("ScrollableCircle").timeout(30s) = [&] { constexpr auto viewSize = CppUtils::Container::Size2{50, 20}; constexpr auto contentSize = CppUtils::Container::Size2{100, 40}; @@ -41,9 +41,9 @@ namespace CppUtils::UnitTest::Terminal::Scrollable { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({38, 8}).character, U'.'); - suite.expectEqual(accessor->getChar({38, 8}).attributes.textColor, CppUtils::Terminal::TextColor::TextColorEnum::Green); - suite.expectEqual(accessor->getChar({49, 0}).character, U'█'); + expect(accessor->getChar({38, 8}).character) == U'.'; + expect(accessor->getChar({38, 8}).attributes.textColor) == CppUtils::Terminal::TextColor::TextColorEnum::Green; + expect(accessor->getChar({49, 0}).character) == U'█'; } constexpr auto maxScrollX = contentSize.width() - viewSize.width(); @@ -54,10 +54,10 @@ namespace CppUtils::UnitTest::Terminal::Scrollable { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({25, 10}).character, U'O'); - suite.expectEqual(accessor->getChar({25, 10}).attributes.backgroundColor, CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Red); - suite.expectEqual(accessor->getChar({25, 0}).character, U'.'); - suite.expectEqual(accessor->getChar({25, 0}).attributes.backgroundColor, CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Blue); + expect(accessor->getChar({25, 10}).character) == U'O'; + expect(accessor->getChar({25, 10}).attributes.backgroundColor) == CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Red; + expect(accessor->getChar({25, 0}).character) == U'.'; + expect(accessor->getChar({25, 0}).attributes.backgroundColor) == CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Blue; } scrollable.setScroll({maxScrollX, maxScrollY}); @@ -65,8 +65,8 @@ namespace CppUtils::UnitTest::Terminal::Scrollable { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'O'); - suite.expectEqual(accessor->getChar({0, 0}).attributes.backgroundColor, CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Red); + expect(accessor->getChar({0, 0}).character) == U'O'; + expect(accessor->getChar({0, 0}).attributes.backgroundColor) == CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Red; } scrollable.setScroll({0, 0}); @@ -94,9 +94,9 @@ namespace CppUtils::UnitTest::Terminal::Scrollable std::this_thread::sleep_for(7ms); } } - }, 30s); + }; - suite.addTest("MouseInteraction", [&] { + suite.test("MouseInteraction") = [&] { constexpr auto viewSize = CppUtils::Container::Size2{50, 20}; constexpr auto contentSize = CppUtils::Container::Size2{100, 40}; @@ -105,7 +105,7 @@ namespace CppUtils::UnitTest::Terminal::Scrollable canvas.print(); std::this_thread::sleep_for(100ms); - suite.expectEqual(scrollable.getScroll(), CppUtils::Container::Size2{0uz, 0uz}); + expect(scrollable.getScroll()) == CppUtils::Container::Size2{0uz, 0uz}; auto clickEvent = CppUtils::Terminal::Mouse::Event{ .button = CppUtils::Terminal::Mouse::Button::Left, @@ -116,7 +116,7 @@ namespace CppUtils::UnitTest::Terminal::Scrollable canvas.print(); std::this_thread::sleep_for(100ms); - suite.expect(scrollable.getScroll().y() > 0); + expect(scrollable.getScroll().y()) > 0; auto dragEvent = clickEvent; dragEvent.dragging = true; @@ -125,7 +125,7 @@ namespace CppUtils::UnitTest::Terminal::Scrollable canvas.print(); std::this_thread::sleep_for(100ms); - suite.expect(scrollable.getScroll().y() > 10); + expect(scrollable.getScroll().y()) > 10; auto scrollUpEvent = CppUtils::Terminal::Mouse::Event{ .button = CppUtils::Terminal::Mouse::Button::ScrollUp, @@ -137,11 +137,11 @@ namespace CppUtils::UnitTest::Terminal::Scrollable canvas.print(); std::this_thread::sleep_for(100ms); - suite.expect(handled); - suite.expect(scrollable.getScroll().y() < prevScrollY); - }); + expect(handled); + expect(scrollable.getScroll().y()) < prevScrollY; + }; - suite.addTest("CustomStyle", [&] { + suite.test("CustomStyle") = [&] { constexpr auto viewSize = CppUtils::Container::Size2{50, 20}; constexpr auto contentSize = CppUtils::Container::Size2{100, 40}; @@ -158,8 +158,8 @@ namespace CppUtils::UnitTest::Terminal::Scrollable { const auto accessor = canvas.getBuffer().sharedAccess(); const auto c = accessor->getChar({viewSize.width() - 1, 10}).character; - suite.expectEqual(c, U'X'); + expect(c) == U'X'; } - }); - }}; + }; + }; } diff --git a/tests/Terminal/Terminal.mpp b/tests/Terminal/Terminal.mpp index 78836b8f..78d3788d 100644 --- a/tests/Terminal/Terminal.mpp +++ b/tests/Terminal/Terminal.mpp @@ -9,10 +9,10 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal { - auto _ = TestSuite{"Terminal", {"UnitTest"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("BackgroundColor", [&] { + suite.test("BackgroundColor") = [&] { auto applyBackgroundColor = []( std::string_view text, @@ -33,9 +33,9 @@ namespace CppUtils::UnitTest::Terminal applyBackgroundColor("Magenta", CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Magenta); applyBackgroundColor("Cyan", CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::Cyan); applyBackgroundColor("White", CppUtils::Terminal::BackgroundColor::BackgroundColorEnum::White); - }); + }; - suite.addTest("TextColor", [&] { + suite.test("TextColor") = [&] { auto applyTextColor = []( std::string_view text, @@ -53,9 +53,9 @@ namespace CppUtils::UnitTest::Terminal applyTextColor("Magenta", CppUtils::Terminal::TextColor::TextColorEnum::Magenta); applyTextColor("Cyan", CppUtils::Terminal::TextColor::TextColorEnum::Cyan); applyTextColor("White", CppUtils::Terminal::TextColor::TextColorEnum::White); - }); + }; - suite.addTest("TextStyle", [&] { + suite.test("TextStyle") = [&] { auto applyTextStyle = []( std::string_view text, @@ -72,18 +72,18 @@ namespace CppUtils::UnitTest::Terminal applyTextStyle("Lighter", CppUtils::Terminal::TextStyle::TextStyleEnum::Lighter); applyTextStyle("Italic", CppUtils::Terminal::TextStyle::TextStyleEnum::Italic); applyTextStyle("Underline", CppUtils::Terminal::TextStyle::TextStyleEnum::Underline); - }); + }; - suite.addTest("eraseCurrentLine", [&] { + suite.test("eraseCurrentLine") = [&] { std::print("Hello World!"); CppUtils::Terminal::TextModifier::eraseCurrentLine(stdout); std::println("Hello!"); - }); + }; - suite.addTest("moveCursorUp", [&] { + suite.test("moveCursorUp") = [&] { std::println("Hello"); CppUtils::Terminal::TextModifier::moveCursorUp(stdout); std::println("Hello World!"); - }); - }}; + }; + }; } diff --git a/tests/Terminal/Text.mpp b/tests/Terminal/Text.mpp index 25191d1e..7d72c392 100644 --- a/tests/Terminal/Text.mpp +++ b/tests/Terminal/Text.mpp @@ -5,8 +5,8 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::Text { - auto _ = TestSuite{"Terminal/Text", {"Terminal/Canvas", "Terminal/Element"}, [](TestSuite& suite) { - suite.addTest("WordWrap", [&] { + auto _ = testSuite("Terminal/Text").dependsOn("Terminal/Canvas", "Terminal/Element") = [](TestSuite& suite) { + suite.test("WordWrap") = [&] { const auto size = CppUtils::Container::Size2{10, 3}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -16,13 +16,13 @@ namespace CppUtils::UnitTest::Terminal::Text { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'H'); - suite.expectEqual(accessor->getChar({0, 1}).character, U'W'); - suite.expectEqual(accessor->getChar({0, 2}).character, U' '); + expect(accessor->getChar({0, 0}).character) == U'H'; + expect(accessor->getChar({0, 1}).character) == U'W'; + expect(accessor->getChar({0, 2}).character) == U' '; } - }); + }; - suite.addTest("NoWordWrap", [&] { + suite.test("NoWordWrap") = [&] { const auto size = CppUtils::Container::Size2{10, 2}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -37,14 +37,14 @@ namespace CppUtils::UnitTest::Terminal::Text { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({0, 0}).character, U'H'); - suite.expectEqual(accessor->getChar({6, 0}).character, U'W'); - suite.expectEqual(accessor->getChar({9, 0}).character, U'l'); - suite.expectEqual(accessor->getChar({0, 1}).character, U' '); + expect(accessor->getChar({0, 0}).character) == U'H'; + expect(accessor->getChar({6, 0}).character) == U'W'; + expect(accessor->getChar({9, 0}).character) == U'l'; + expect(accessor->getChar({0, 1}).character) == U' '; } - }); + }; - suite.addTest("OverflowEllipsis", [&] { + suite.test("OverflowEllipsis") = [&] { const auto size = CppUtils::Container::Size2{10, 2}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -54,11 +54,11 @@ namespace CppUtils::UnitTest::Terminal::Text { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({5, 1}).character, U'…'); + expect(accessor->getChar({5, 1}).character) == U'…'; } - }); + }; - suite.addTest("OverflowClip", [&] { + suite.test("OverflowClip") = [&] { const auto size = CppUtils::Container::Size2{10, 1}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -73,11 +73,11 @@ namespace CppUtils::UnitTest::Terminal::Text { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({9, 0}).character, U'u'); + expect(accessor->getChar({9, 0}).character) == U'u'; } - }); + }; - suite.addTest("Alignments", [&] { + suite.test("Alignments") = [&] { const auto size = CppUtils::Container::Size2{10, 3}; auto canvas = CppUtils::Terminal::Canvas{size}; @@ -88,9 +88,9 @@ namespace CppUtils::UnitTest::Terminal::Text { const auto accessor = canvas.getBuffer().sharedAccess(); - suite.expectEqual(accessor->getChar({4, 1}).character, U'H'); - suite.expectEqual(accessor->getChar({5, 1}).character, U'i'); + expect(accessor->getChar({4, 1}).character) == U'H'; + expect(accessor->getChar({5, 1}).character) == U'i'; } - }); - }}; + }; + }; } diff --git a/tests/Terminal/Title.mpp b/tests/Terminal/Title.mpp index 001f6459..56d9a47a 100644 --- a/tests/Terminal/Title.mpp +++ b/tests/Terminal/Title.mpp @@ -5,21 +5,21 @@ import CppUtils; namespace CppUtils::UnitTest::Terminal::Title { - auto _ = TestSuite{"Terminal", {"UnitTest"}, [](TestSuite& suite) { + auto _ = testSuite("Terminal").dependsOn("UnitTest") = [](TestSuite& suite) { // using namespace std::literals; // using namespace std::chrono_literals; - suite.addTest("Title", [&] { + suite.test("Title") = [&] { /* auto oldTitle = CppUtils::Terminal::getTerminalTitle(); { auto newTitle = "Terminal Title"sv; auto _ = CppUtils::Terminal::Title{newTitle}; - suite.expectEqual(CppUtils::Terminal::getTerminalTitle(), newTitle); + expect(CppUtils::Terminal::getTerminalTitle()) == newTitle; std::this_thread::sleep_for(3s); } - suite.expectEqual(CppUtils::Terminal::getTerminalTitle(), oldTitle); + expect(CppUtils::Terminal::getTerminalTitle()) == oldTitle; */ - }); - }}; + }; + }; } diff --git a/tests/Terminal/Utility.mpp b/tests/Terminal/Utility.mpp index ac739f74..41014a39 100644 --- a/tests/Terminal/Utility.mpp +++ b/tests/Terminal/Utility.mpp @@ -32,48 +32,48 @@ namespace CppUtils::UnitTest::Terminal::Utility #endif } - auto _ = TestSuite{"Terminal/Utility", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("getState and setState", [&] { + auto _ = testSuite("Terminal/Utility").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test("getState and setState") = [&] { const auto state = CppUtils::Terminal::getState(); if (state) - suite.expect(CppUtils::Terminal::setState(state.value())); - }); + expect(CppUtils::Terminal::setState(state.value())); + }; - suite.addTest("isRawMode", [&] { - suite.expectEqual(CppUtils::Terminal::isRawMode(), directOSIsRawMode()); - }); + suite.test("isRawMode") = [&] { + expect(CppUtils::Terminal::isRawMode()) == directOSIsRawMode(); + }; - suite.addTest("enableRawMode toggles raw mode and enables restoration", [&] { + suite.test("enableRawMode toggles raw mode and enables restoration") = [&] { const auto initialRaw = CppUtils::Terminal::isRawMode(); const auto oldState = CppUtils::Terminal::enableRawMode(); - suite.skipIf(not oldState, "Terminal environment non-interactive"); + skipIf(not oldState, "Terminal environment non-interactive"); - suite.expect(CppUtils::Terminal::isRawMode()); - suite.expect(directOSIsRawMode()); + expect(CppUtils::Terminal::isRawMode()); + expect(directOSIsRawMode()); - suite.expect(CppUtils::Terminal::setState(oldState.value())); - suite.expectEqual(CppUtils::Terminal::isRawMode(), initialRaw); - suite.expectEqual(directOSIsRawMode(), initialRaw); - }); + expect(CppUtils::Terminal::setState(oldState.value())); + expect(CppUtils::Terminal::isRawMode()) == initialRaw; + expect(directOSIsRawMode()) == initialRaw; + }; - suite.addTest("Flag bitwise operators", [&] { + suite.test("Flag bitwise operators") = [&] { using CppUtils::Terminal::Flag; const auto combined = Flag::WaitForEnter | Flag::PrintTypedKeys; - suite.expect(combined & Flag::WaitForEnter); - suite.expect(combined & Flag::PrintTypedKeys); - }); + expect(combined & Flag::WaitForEnter); + expect(combined & Flag::PrintTypedKeys); + }; - suite.addTest("Cross-platform unified flags", [&] { + suite.test("Cross-platform unified flags") = [&] { using CppUtils::Terminal::Flag; const auto allFlags = Flag::None | Flag::WaitForEnter | Flag::PrintTypedKeys | Flag::ProcessInterruptSignals | Flag::TextEditingShortcuts | Flag::MouseInput | Flag::MouseTextSelection | Flag::AllowMouseSelection | Flag::AnsiEscapeCodes | Flag::Default; - suite.expect(allFlags & Flag::WaitForEnter); - suite.expect(Flag::Default & Flag::WaitForEnter); - suite.expect(Flag::Default & Flag::PrintTypedKeys); - }); + expect(allFlags & Flag::WaitForEnter); + expect(Flag::Default & Flag::WaitForEnter); + expect(Flag::Default & Flag::PrintTypedKeys); + }; - suite.addTest("hasFlag function", [&] { + suite.test("hasFlag function") = [&] { using CppUtils::Terminal::Flag; - suite.expectEqual(CppUtils::Terminal::hasFlag(Flag::Raw), CppUtils::Terminal::isRawMode()); - }); - }}; + expect(CppUtils::Terminal::hasFlag(Flag::Raw)) == CppUtils::Terminal::isRawMode(); + }; + }; } diff --git a/tests/Thread/AsyncEventDispatcher.mpp b/tests/Thread/AsyncEventDispatcher.mpp index b7c53f7f..9ea05619 100644 --- a/tests/Thread/AsyncEventDispatcher.mpp +++ b/tests/Thread/AsyncEventDispatcher.mpp @@ -5,20 +5,20 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::AsyncEventDispatcher { - auto _ = TestSuite{"Thread/AsyncEventDispatcher", {"Thread/ThreadPool", "Execution/EventDispatcher"}, [](TestSuite& suite) { - suite.addTest("Emit and Wait", [&] { + auto _ = testSuite("Thread/AsyncEventDispatcher").dependsOn("Thread/ThreadPool", "Execution/EventDispatcher") = [](TestSuite& suite) { + suite.test("Emit and Wait") = [&] { auto dispatcher = CppUtils::Thread::AsyncEventDispatcher{}; auto received = std::atomic_bool{false}; - dispatcher.subscribe<"Event">([&](bool value) { - suite.expectEqual(value, true); + dispatcher.subscribe<"Event">([&](bool receivedValue) { + expect(receivedValue); received = true; }); dispatcher.emit<"Event">(true); dispatcher.waitUntilFinished(); - suite.expect(received); - }); + expect(received); + }; // Workaround on Windows (MSVC ABI) // https://github.com/llvm/llvm-project/issues/221609 @@ -39,7 +39,7 @@ namespace CppUtils::UnitTest::Thread::AsyncEventDispatcher using TestException = std::runtime_error; #endif - suite.addTest("Error Handling", [&] { + suite.test("Error Handling") = [&] { auto errorCaught = std::atomic_bool{false}; auto onError = [&](std::exception_ptr) { errorCaught = true; @@ -52,10 +52,10 @@ namespace CppUtils::UnitTest::Thread::AsyncEventDispatcher dispatcher.emit<"Event">(nullptr); dispatcher.waitUntilFinished(); - suite.expect(errorCaught); - }); + expect(errorCaught); + }; - suite.addTest("Stateful lambda", [&] { + suite.test("Stateful lambda") = [&] { auto dispatcher = CppUtils::Thread::AsyncEventDispatcher{}; auto counter = std::make_shared(0); auto mutex = std::make_shared(); @@ -69,10 +69,10 @@ namespace CppUtils::UnitTest::Thread::AsyncEventDispatcher dispatcher.emit<"Event">(20); dispatcher.waitUntilFinished(); - suite.expectEqual(*counter, 30); - }); + expect(*counter) == 30; + }; - suite.addTest("Passing stateful lambda as argument", [&] { + suite.test("Passing stateful lambda as argument") = [&] { auto dispatcher = CppUtils::Thread::AsyncEventDispatcher{}; auto result = std::atomic_int{0}; @@ -86,10 +86,10 @@ namespace CppUtils::UnitTest::Thread::AsyncEventDispatcher }}); dispatcher.waitUntilFinished(); - suite.expectEqual(result.load(), 42); - }); + expect(result.load()) == 42; + }; - suite.addTest("Multiple subscribe", [&] { + suite.test("Multiple subscribe") = [&] { auto dispatcher = CppUtils::Thread::AsyncEventDispatcher{}; auto count = std::atomic_size_t{0}; @@ -102,7 +102,7 @@ namespace CppUtils::UnitTest::Thread::AsyncEventDispatcher dispatcher.emit<"C">(); dispatcher.waitUntilFinished(); - suite.expectEqual(count, 2uz); - }); - }}; + expect(count) == 2uz; + }; + }; } diff --git a/tests/Thread/Poll.mpp b/tests/Thread/Poll.mpp index 76a78597..fe20630c 100644 --- a/tests/Thread/Poll.mpp +++ b/tests/Thread/Poll.mpp @@ -5,39 +5,39 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::Poll { - auto _ = TestSuite{"Thread/Poll", [](TestSuite& suite) { + auto _ = testSuite("Thread/Poll") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Immediate success", [&] { + suite.test("Immediate success") = [&] { auto counter = 0uz; const auto result = CppUtils::Thread::pollFor([&] { ++counter; return true; }, 100ms, 10ms); - suite.expect(result); - suite.expectEqual(counter, 1uz); - }); + expect(result); + expect(counter) == 1uz; + }; - suite.addTest("Delayed success", [&] { + suite.test("Delayed success") = [&] { auto counter = 0uz; const auto result = CppUtils::Thread::pollFor([&] { return ++counter >= 3uz; }, 500ms, 10ms); - suite.expect(result); - suite.expectEqual(counter, 3uz); - }); + expect(result); + expect(counter) == 3uz; + }; - suite.addTest("Timeout failure", [&] { + suite.test("Timeout failure") = [&] { auto counter = 0uz; const auto result = CppUtils::Thread::pollFor([&] { ++counter; return false; }, 50ms, 10ms); - suite.expect(not result); - suite.expect(counter >= 1uz); - }); + expect(not result); + expect(counter) >= 1uz; + }; - suite.addTest("pollUntil with deadline", [&] { + suite.test("pollUntil with deadline") = [&] { auto flag = false; auto thread = std::jthread{[&](std::stop_token) { std::this_thread::sleep_for(30ms); @@ -48,8 +48,8 @@ namespace CppUtils::UnitTest::Thread::Poll const auto result = CppUtils::Thread::pollUntil([&] { return flag; }, deadline, 10ms); - suite.expect(result); - suite.expect(flag); - }); - }}; + expect(result); + expect(flag); + }; + }; } diff --git a/tests/Thread/ScheduledEventDispatcher.mpp b/tests/Thread/ScheduledEventDispatcher.mpp index 87a7d9c0..7029e61f 100644 --- a/tests/Thread/ScheduledEventDispatcher.mpp +++ b/tests/Thread/ScheduledEventDispatcher.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::ScheduledEventDispatcher { - auto _ = TestSuite{"Thread/ScheduledEventDispatcher", {"Execution/EventDispatcher", "Thread/Scheduler"}, [](TestSuite& suite) { + auto _ = testSuite("Thread/ScheduledEventDispatcher").dependsOn("Execution/EventDispatcher", "Thread/Scheduler") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Immediate Emit", [&] { + suite.test("Immediate Emit") = [&] { auto dispatcher = CppUtils::Thread::ScheduledEventDispatcher{}; auto receivedValue = std::atomic_bool{}; @@ -18,10 +18,10 @@ namespace CppUtils::UnitTest::Thread::ScheduledEventDispatcher dispatcher.emit<"Event">(true); dispatcher.waitUntilFinished(); - suite.expectEqual(receivedValue.load(), true); - }); + expect(receivedValue.load()); + }; - suite.addTest("Delayed Emit", [&] { + suite.test("Delayed Emit") = [&] { auto dispatcher = CppUtils::Thread::ScheduledEventDispatcher{}; auto receivedValue = std::atomic_bool{}; auto executionTime = std::atomic{}; @@ -36,15 +36,15 @@ namespace CppUtils::UnitTest::Thread::ScheduledEventDispatcher dispatcher.emit<"Event">(delay, true); if (CppUtils::Thread::ScheduledEventDispatcher::Clock::now() < startTime + delay) - suite.expectEqual(receivedValue.load(), false); + not expect(receivedValue.load()); dispatcher.waitUntilFinished(); - suite.expectEqual(receivedValue.load(), true); - suite.expect(executionTime.load() >= startTime + delay); - }); + expect(receivedValue.load()); + expect(executionTime.load()) >= startTime + delay; + }; - suite.addTest("Delayed Emit with Flush", [&] { + suite.test("Delayed Emit with Flush") = [&] { auto dispatcher = CppUtils::Thread::ScheduledEventDispatcher{}; auto receivedValue = std::atomic_bool{}; @@ -53,9 +53,9 @@ namespace CppUtils::UnitTest::Thread::ScheduledEventDispatcher }); dispatcher.emit<"Event">(30s, true); - suite.expect(not receivedValue.load()); + expect(not receivedValue.load()); dispatcher.flush(); - suite.expect(receivedValue.load()); - }); - }}; + expect(receivedValue.load()); + }; + }; } diff --git a/tests/Thread/Scheduler.mpp b/tests/Thread/Scheduler.mpp index f168e33b..be5c735b 100644 --- a/tests/Thread/Scheduler.mpp +++ b/tests/Thread/Scheduler.mpp @@ -5,16 +5,16 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::Scheduler { - auto _ = TestSuite{"Thread/Scheduler", {"UnitTest"}, []([[maybe_unused]] auto& suite) { + auto _ = testSuite("Thread/Scheduler").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace std::chrono_literals; - suite.addTest("Initialize Scheduler", [&] { + suite.test("Initialize Scheduler") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; scheduler.waitUntilFinished(); - }); + }; - suite.addTest("Schedule and execute", [&] { + suite.test("Schedule and execute") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; auto executed = std::atomic_bool{false}; @@ -24,10 +24,10 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expect(executed.load()); - }); + expect(executed.load()); + }; - suite.addTest("Recursive scheduling", [&] { + suite.test("Recursive scheduling") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; auto counter = std::atomic_size_t{0}; @@ -40,10 +40,10 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expectEqual(counter.load(), 2uz); - }); + expect(counter.load()) == 2uz; + }; - suite.addTest("Cancel tasks on destruction", [&] { + suite.test("Cancel tasks on destruction") = [&] { auto executed = std::atomic_bool{false}; { @@ -54,10 +54,10 @@ namespace CppUtils::UnitTest::Thread::Scheduler }, 100ms); } - suite.expect(not executed.load()); - }); + expect(not executed.load()); + }; - suite.addTest("Multiple tasks", [&] { + suite.test("Multiple tasks") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; auto counter = std::atomic_size_t{0}; @@ -67,10 +67,10 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expectEqual(counter.load(), 3uz); - }); + expect(counter.load()) == 3uz; + }; - suite.addTest("Cancel specific task", [&] { + suite.test("Cancel specific task") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; auto number = std::atomic_size_t{0}; @@ -82,25 +82,25 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expectEqual(number.load(), 5uz); - }); + expect(number.load()) == 5uz; + }; - suite.addTest("Cancel all tasks", [&] { + suite.test("Cancel all tasks") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; auto number = std::atomic_size_t{0}; - scheduler.schedule([&number] { ++number; }, 0ms); - scheduler.schedule([&number] { number += 0b10; }, 250ms); + scheduler.schedule([&number] { ++number; }, 200ms); + scheduler.schedule([&number] { number += 0b10; }, 350ms); scheduler.schedule([&number] { number += 0b100; }, 500ms); scheduler.cancelAll(); scheduler.waitUntilFinished(); - suite.expectEqual(number.load(), 0uz); - }); + expect(number.load()) == 0uz; + }; - suite.addTest("Cancel tasks by owner", [&] { + suite.test("Cancel tasks by owner") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; auto number1 = std::atomic_size_t{0}; auto number2 = std::atomic_size_t{0}; @@ -115,11 +115,11 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expectEqual(number1.load(), 0uz); - suite.expectEqual(number2.load(), 1uz); - }); + expect(number1.load()) == 0uz; + expect(number2.load()) == 1uz; + }; - suite.addTest("Order of execution", [&] { + suite.test("Order of execution") = [&] { auto scheduler = CppUtils::Thread::Scheduler{0s, 1uz}; auto executionOrder = std::vector{}; auto mutex = std::mutex{}; @@ -135,13 +135,13 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expectEqual(std::ranges::size(executionOrder), 3uz); - suite.expectEqual(executionOrder[0], 2uz); - suite.expectEqual(executionOrder[1], 1uz); - suite.expectEqual(executionOrder[2], 3uz); - }); + expect(std::ranges::size(executionOrder)) == 3uz; + expect(executionOrder[0]) == 2uz; + expect(executionOrder[1]) == 1uz; + expect(executionOrder[2]) == 3uz; + }; - suite.addTest("Without bucketing", [&] { + suite.test("Without bucketing") = [&] { using Clock = CppUtils::Thread::Scheduler::Clock; using TimePoint = CppUtils::Thread::Scheduler::TimePoint; auto scheduler = CppUtils::Thread::Scheduler{0s, 1uz}; @@ -161,15 +161,15 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expectEqual(std::ranges::size(times), 4uz); + expect(std::ranges::size(times)) == 4uz; // Chaque tache respecte sa date - suite.expect(times[0] >= now + 80ms); - suite.expect(times[1] >= now + 300ms); - suite.expect(times[2] >= now + 650ms); - suite.expect(times[3] >= now + 1100ms); - }); + expect(times[0]) >= now + 80ms; + expect(times[1]) >= now + 300ms; + expect(times[2]) >= now + 650ms; + expect(times[3]) >= now + 1100ms; + }; - suite.addTest("With bucketing", [&] { + suite.test("With bucketing") = [&] { using Clock = CppUtils::Thread::Scheduler::Clock; using TimePoint = CppUtils::Thread::Scheduler::TimePoint; auto scheduler = CppUtils::Thread::Scheduler{300ms, 1uz}; @@ -189,16 +189,16 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expectEqual(std::ranges::size(times), 4uz); + expect(std::ranges::size(times)) == 4uz; // Avec bucketing (300ms): les taches 0 (100ms) et 1 (350ms) sont groupées dans le 1er bucket (< 80ms) - suite.expect(times[1] - times[0] < 80ms); + expect(times[1] - times[0]) < 80ms; // La tache 2 (700ms) est dans le 2ème bucket - suite.expect(times[2] - times[1] >= 150ms); + expect(times[2] - times[1]) >= 150ms; // La tache 3 (1200ms) est dans le 3ème bucket - suite.expect(times[3] - times[2] >= 150ms); - }); + expect(times[3] - times[2]) >= 150ms; + }; - suite.addTest("Schedule events in reverse order", [&] { + suite.test("Schedule events in reverse order") = [&] { auto scheduler = CppUtils::Thread::Scheduler{0s, 1uz}; auto executionOrder = std::vector{}; auto mutex = std::mutex{}; @@ -213,27 +213,26 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expectEqual(std::ranges::size(executionOrder), 2uz); - suite.expectEqual(executionOrder[0], 1uz); - suite.expectEqual(executionOrder[1], 2uz); - }); + expect(std::ranges::size(executionOrder)) == 2uz; + expect(executionOrder[0]) == 1uz; + expect(executionOrder[1]) == 2uz; + }; - suite.addTest("Schedule earlier task after later task", [&] { + suite.test("Schedule earlier task after later task") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; - auto aExecuted = std::atomic_bool{false}; + auto taskExecutionSignal = std::binary_semaphore{0}; auto bExecuted = std::atomic_bool{false}; scheduler.schedule([&] { bExecuted = true; }, 500ms); - scheduler.schedule([&] { aExecuted = true; }, 100ms); + scheduler.schedule([&] { taskExecutionSignal.release(); }, 100ms); - std::this_thread::sleep_for(250ms); + expect(taskExecutionSignal) == completes; scheduler.cancelAll(); - suite.expect(aExecuted.load()); - suite.expect(not bExecuted.load()); - }); + expect(not bExecuted.load()); + }; - suite.addTest("Stress test with many tasks", [&] { + suite.test("Stress test with many tasks") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; const auto numTasks = 10'000uz; auto executedCount = std::atomic_size_t{0}; @@ -249,8 +248,8 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expectEqual(executedCount.load(), numTasks); - }); + expect(executedCount.load()) == numTasks; + }; // Workaround on Windows (MSVC ABI) // https://github.com/llvm/llvm-project/issues/221609 @@ -271,7 +270,7 @@ namespace CppUtils::UnitTest::Thread::Scheduler using TestException = std::runtime_error; #endif - suite.addTest("onError callback is invoked on exception", [&] { + suite.test("onError callback is invoked on exception") = [&] { auto onErrorInvoked = std::atomic_bool{false}; auto scheduler = CppUtils::Thread::Scheduler{ 0ms, @@ -286,25 +285,25 @@ namespace CppUtils::UnitTest::Thread::Scheduler scheduler.waitUntilFinished(); - suite.expect(onErrorInvoked.load()); - }); + expect(onErrorInvoked.load()); + }; - suite.addTest("getScheduledTasksCount(): Waiting tasks", [&] { + suite.test("getScheduledTasksCount(): Waiting tasks") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; - suite.expectEqual(scheduler.getScheduledTasksCount(), 0uz); + expect(scheduler.getScheduledTasksCount()) == 0uz; scheduler.schedule([] {}, 10s); - suite.expectEqual(scheduler.getScheduledTasksCount(), 1uz); + expect(scheduler.getScheduledTasksCount()) == 1uz; scheduler.schedule([] {}, 20s); - suite.expectEqual(scheduler.getScheduledTasksCount(), 2uz); + expect(scheduler.getScheduledTasksCount()) == 2uz; scheduler.cancelAll(); - suite.expectEqual(scheduler.getScheduledTasksCount(), 0uz); - }); + expect(scheduler.getScheduledTasksCount()) == 0uz; + }; - suite.addTest("getScheduledTasksCount(): Processing tasks", [&] { + suite.test("getScheduledTasksCount(): Processing tasks") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; auto promise = std::promise{}; @@ -318,14 +317,14 @@ namespace CppUtils::UnitTest::Thread::Scheduler }, 0s); startedFuture.wait(); - suite.expectEqual(scheduler.getScheduledTasksCount(), 1uz); + expect(scheduler.getScheduledTasksCount()) == 1uz; promise.set_value(); scheduler.flush(); - suite.expectEqual(scheduler.getScheduledTasksCount(), 0uz); - }); + expect(scheduler.getScheduledTasksCount()) == 0uz; + }; - suite.addTest("Flush tasks", [&] { + suite.test("Flush tasks") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; auto executed = std::atomic_bool{false}; @@ -333,12 +332,12 @@ namespace CppUtils::UnitTest::Thread::Scheduler executed = true; }, 30s); - suite.expect(not executed.load()); + expect(not executed.load()); scheduler.flush(); - suite.expect(executed.load()); - }); + expect(executed.load()); + }; - suite.addTest("Flush tasks with chaining", [&] { + suite.test("Flush tasks with chaining") = [&] { auto scheduler = CppUtils::Thread::Scheduler{}; auto counter = std::atomic_size_t{0}; @@ -349,9 +348,9 @@ namespace CppUtils::UnitTest::Thread::Scheduler }, 30s); }, 30s); - suite.expectEqual(counter.load(), 0uz); + expect(counter.load()) == 0uz; scheduler.flush(); - suite.expectEqual(counter.load(), 2uz); - }); - }}; + expect(counter.load()) == 2uz; + }; + }; } diff --git a/tests/Thread/SharedLocker.mpp b/tests/Thread/SharedLocker.mpp index ed86a914..47312fdd 100644 --- a/tests/Thread/SharedLocker.mpp +++ b/tests/Thread/SharedLocker.mpp @@ -5,16 +5,16 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::SharedLocker { - auto _ = TestSuite{"Thread/SharedLocker", {"UnitTest", "Thread/UniqueLocker"}, [](TestSuite& suite) { - suite.addTest("Unique access", [&] { + auto _ = testSuite("Thread/SharedLocker").dependsOn("UnitTest", "Thread/UniqueLocker") = [](TestSuite& suite) { + suite.test("Unique access") = [&] { auto lockedString = CppUtils::Thread::SharedLocker{"Foo"}; { auto accessor = lockedString.uniqueAccess(); - suite.expectEqual(accessor.value(), "Foo"); + expect(accessor.value()) == "Foo"; } - }); + }; - suite.addTest("Unique access: Test mutex", [&] { + suite.test("Unique access: Test mutex") = [&] { auto lockedNumber = CppUtils::Thread::SharedLocker{0uz}; auto incrementFunction = [&lockedNumber](std::size_t iterations) noexcept -> void { @@ -32,57 +32,57 @@ namespace CppUtils::UnitTest::Thread::SharedLocker auto thread1 = std::jthread{incrementFunction, iterations}; } auto accessor = lockedNumber.uniqueAccess(); - suite.expectEqual(accessor.value(), iterations * 2); - }); + expect(accessor.value()) == iterations * 2; + }; - suite.addTest("Shared access", [&] { + suite.test("Shared access") = [&] { auto lockedString = CppUtils::Thread::SharedLocker{"Foo"}; { auto accessor0 = lockedString.sharedAccess(); auto accessor1 = lockedString.sharedAccess(); - suite.expectEqual(accessor0.value(), "Foo"); - suite.expectEqual(accessor1.value(), "Foo"); + expect(accessor0.value()) == "Foo"; + expect(accessor1.value()) == "Foo"; } - }); + }; - suite.addTest("MultipleAccessor", [&] { + suite.test("MultipleAccessor") = [&] { auto lockedString1 = CppUtils::Thread::SharedLocker{"Foo"}; auto lockedString2 = CppUtils::Thread::SharedLocker{"Bar"}; { auto accessor = CppUtils::Thread::MultipleAccessor{lockedString1, lockedString2}; - suite.expectEqual(std::get<0>(accessor.values), "Foo"); - suite.expectEqual(std::get<1>(accessor.values), "Bar"); + expect(std::get<0>(accessor.values)) == "Foo"; + expect(std::get<1>(accessor.values)) == "Bar"; } - }); + }; - suite.addTest("Mixed access", [&] { + suite.test("Mixed access") = [&] { auto lockedString1 = CppUtils::Thread::UniqueLocker{"Foo"}; auto lockedString2 = CppUtils::Thread::SharedLocker{"Bar"}; { auto accessor = CppUtils::Thread::MultipleAccessor{lockedString1, lockedString2}; - suite.expectEqual(std::get<0>(accessor.values), "Foo"); - suite.expectEqual(std::get<1>(accessor.values), "Bar"); + expect(std::get<0>(accessor.values)) == "Foo"; + expect(std::get<1>(accessor.values)) == "Bar"; } - }); + }; - suite.addTest("Move constructor", [&] { + suite.test("Move constructor") = [&] { auto source = CppUtils::Thread::SharedLocker>{std::make_unique(42)}; auto destination = CppUtils::Thread::SharedLocker>{std::move(source)}; - suite.expect(source.uniqueAccess().value() == nullptr); - suite.expect(destination.sharedAccess().value() != nullptr); - suite.expectEqual(*destination.sharedAccess().value(), 42); - }); + expect(source.uniqueAccess().value()) == nullptr; + expect(destination.sharedAccess().value()) != nullptr; + expect(*destination.sharedAccess().value()) == 42; + }; - suite.addTest("Move assignment", [&] { + suite.test("Move assignment") = [&] { auto source = CppUtils::Thread::SharedLocker>{std::make_unique(100)}; auto destination = CppUtils::Thread::SharedLocker>{std::make_unique(200)}; destination = std::move(source); - suite.expect(source.uniqueAccess().value() == nullptr); - suite.expect(destination.sharedAccess().value() != nullptr); - suite.expectEqual(*destination.sharedAccess().value(), 100); - }); - }}; + expect(source.uniqueAccess().value()) == nullptr; + expect(destination.sharedAccess().value()) != nullptr; + expect(*destination.sharedAccess().value()) == 100; + }; + }; } diff --git a/tests/Thread/SharedPtr.mpp b/tests/Thread/SharedPtr.mpp index 88c62df9..01128f9e 100644 --- a/tests/Thread/SharedPtr.mpp +++ b/tests/Thread/SharedPtr.mpp @@ -4,150 +4,150 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::SharedPtr { - static_assert([] { - auto shared = CppUtils::Thread::makeShared(42); - if (not shared or shared.use_count() != 1uz) - return false; + auto _ = testSuite("Thread/SharedPtr").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test<[] { + auto shared = CppUtils::Thread::makeShared(42); + if (not shared or shared.use_count() != 1uz) + return false; - auto copy = shared; - if (shared.use_count() != 2uz or *copy != 42) - return false; + auto copy = shared; + if (shared.use_count() != 2uz or *copy != 42) + return false; - *copy = 100; - if (*shared != 100) - return false; + *copy = 100; + if (*shared != 100) + return false; - auto weak = CppUtils::Thread::WeakPtr{shared}; - if (weak.expired() or weak.use_count() != 2uz) - return false; + auto weak = CppUtils::Thread::WeakPtr{shared}; + if (weak.expired() or weak.use_count() != 2uz) + return false; - auto locked = weak.lock(); - if (not locked or locked.use_count() != 3uz or *locked != 100) - return false; + auto locked = weak.lock(); + if (not locked or locked.use_count() != 3uz or *locked != 100) + return false; - return true; - }()); + return true; + }>("Constexpr SharedPtr and WeakPtr"); - auto _ = TestSuite{"Thread/SharedPtr", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("Empty", [&] { + suite.test("Empty") = [&] { auto sharedInt = CppUtils::Thread::SharedPtr{}; - suite.expect(not sharedInt); - suite.expectEqual(sharedInt.use_count(), 0uz); - suite.expect(sharedInt == nullptr); - suite.expect(nullptr == sharedInt); - }); + expect(not sharedInt); + expect(sharedInt.use_count()) == 0uz; + expect(sharedInt) == nullptr; + expect(nullptr) == sharedInt; + }; - suite.addTest("Construction and dereference", [&] { + suite.test("Construction and dereference") = [&] { auto shared = CppUtils::Thread::makeShared("Antigravity"); - suite.expect(static_cast(shared)); - suite.expectEqual(shared.use_count(), 1uz); - suite.expectEqual(*shared, "Antigravity"); - suite.expectEqual(shared->size(), 11uz); - }); + expect(static_cast(shared)); + expect(shared.use_count()) == 1uz; + expect(*shared) == "Antigravity"; + expect(shared->size()) == 11uz; + }; - suite.addTest("Copy semantics and refcount", [&] { + suite.test("Copy semantics and refcount") = [&] { auto shared1 = CppUtils::Thread::makeShared(123); - suite.expectEqual(shared1.use_count(), 1uz); + expect(shared1.use_count()) == 1uz; { auto shared2 = shared1; - suite.expectEqual(shared1.use_count(), 2uz); - suite.expectEqual(shared2.use_count(), 2uz); - suite.expectEqual(*shared2, 123); + expect(shared1.use_count()) == 2uz; + expect(shared2.use_count()) == 2uz; + expect(*shared2) == 123; auto shared3 = CppUtils::Thread::SharedPtr{}; shared3 = shared2; - suite.expectEqual(shared1.use_count(), 3uz); + expect(shared1.use_count()) == 3uz; } - suite.expectEqual(shared1.use_count(), 1uz); - }); + expect(shared1.use_count()) == 1uz; + }; - suite.addTest("Move semantics", [&] { + suite.test("Move semantics") = [&] { auto shared1 = CppUtils::Thread::makeShared(456); - suite.expectEqual(shared1.use_count(), 1uz); + expect(shared1.use_count()) == 1uz; auto shared2 = std::move(shared1); - suite.expect(not shared1); - suite.expectEqual(shared1.use_count(), 0uz); - suite.expect(static_cast(shared2)); - suite.expectEqual(shared2.use_count(), 1uz); - suite.expectEqual(*shared2, 456); + expect(not shared1); + expect(shared1.use_count()) == 0uz; + expect(static_cast(shared2)); + expect(shared2.use_count()) == 1uz; + expect(*shared2) == 456; auto shared3 = CppUtils::Thread::SharedPtr{}; shared3 = std::move(shared2); - suite.expect(not shared2); - suite.expectEqual(shared3.use_count(), 1uz); - suite.expectEqual(*shared3, 456); - }); + expect(not shared2); + expect(shared3.use_count()) == 1uz; + expect(*shared3) == 456; + }; - suite.addTest("Reset", [&] { + suite.test("Reset") = [&] { auto shared = CppUtils::Thread::makeShared(789); - suite.expectEqual(shared.use_count(), 1uz); + expect(shared.use_count()) == 1uz; shared.reset(); - suite.expect(not shared); - suite.expectEqual(shared.use_count(), 0uz); - suite.expect(shared == nullptr); - }); + expect(not shared); + expect(shared.use_count()) == 0uz; + expect(shared) == nullptr; + }; - suite.addTest("WeakPtr basic and lock", [&] { + suite.test("WeakPtr basic and lock") = [&] { auto shared = CppUtils::Thread::makeShared("WeakValue"); auto weak = CppUtils::Thread::WeakPtr{shared}; - suite.expect(not weak.expired()); - suite.expectEqual(weak.use_count(), 1uz); + expect(not weak.expired()); + expect(weak.use_count()) == 1uz; auto locked = weak.lock(); - suite.expect(static_cast(locked)); - suite.expectEqual(locked.use_count(), 2uz); - suite.expectEqual(*locked, "WeakValue"); - }); + expect(static_cast(locked)); + expect(locked.use_count()) == 2uz; + expect(*locked) == "WeakValue"; + }; - suite.addTest("WeakPtr expiration and destruction", [&] { + suite.test("WeakPtr expiration and destruction") = [&] { auto weak = CppUtils::Thread::WeakPtr{}; - suite.expect(weak.expired()); - suite.expectEqual(weak.use_count(), 0uz); + expect(weak.expired()); + expect(weak.use_count()) == 0uz; { auto shared = CppUtils::Thread::makeShared("Temporary"); weak = shared; - suite.expect(not weak.expired()); - suite.expectEqual(weak.use_count(), 1uz); + expect(not weak.expired()); + expect(weak.use_count()) == 1uz; } - suite.expect(weak.expired()); - suite.expectEqual(weak.use_count(), 0uz); + expect(weak.expired()); + expect(weak.use_count()) == 0uz; auto locked = weak.lock(); - suite.expect(not locked); - suite.expect(locked == nullptr); - }); + expect(not locked); + expect(locked) == nullptr; + }; - suite.addTest("Comparison and owner equality", [&] { + suite.test("Comparison and owner equality") = [&] { auto shared1 = CppUtils::Thread::makeShared(10); auto shared2 = shared1; auto shared3 = CppUtils::Thread::makeShared(10); - suite.expect(shared1 == shared2); - suite.expect(shared1.owner_equal(shared2)); - suite.expect(CppUtils::Thread::ownerEqual(shared1, shared2)); - suite.expect(not(shared1 == shared3)); - suite.expect(not shared1.owner_equal(shared3)); - suite.expect(not CppUtils::Thread::ownerEqual(shared1, shared3)); - suite.expect(shared1 != nullptr); - suite.expect(nullptr != shared1); + expect(shared1) == shared2; + expect(shared1.owner_equal(shared2)); + expect(CppUtils::Thread::ownerEqual(shared1, shared2)); + expect(not(shared1 == shared3)); + expect(not shared1.owner_equal(shared3)); + expect(not CppUtils::Thread::ownerEqual(shared1, shared3)); + expect(shared1) != nullptr; + expect(nullptr) != shared1; auto weak1 = CppUtils::Thread::WeakPtr{shared1}; auto weak2 = CppUtils::Thread::WeakPtr{shared2}; auto weak3 = CppUtils::Thread::WeakPtr{shared3}; - suite.expect(weak1 == weak2); - suite.expect(not(weak1 == weak3)); - suite.expect(weak1.owner_equal(weak2)); - suite.expect(CppUtils::Thread::ownerEqual(weak1, weak2)); - suite.expect(not weak1.owner_equal(weak3)); - suite.expect(not CppUtils::Thread::ownerEqual(weak1, weak3)); - }); - }}; + expect(weak1) == weak2; + expect(not(weak1 == weak3)); + expect(weak1.owner_equal(weak2)); + expect(CppUtils::Thread::ownerEqual(weak1, weak2)); + expect(not weak1.owner_equal(weak3)); + expect(not CppUtils::Thread::ownerEqual(weak1, weak3)); + }; + }; } diff --git a/tests/Thread/ThreadLoop.mpp b/tests/Thread/ThreadLoop.mpp index 2bc531cf..d0871704 100644 --- a/tests/Thread/ThreadLoop.mpp +++ b/tests/Thread/ThreadLoop.mpp @@ -5,25 +5,25 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::ThreadLoop { - auto _ = TestSuite{"Thread/ThreadLoop", {"UnitTest"}, [](TestSuite& suite) { + auto _ = testSuite("Thread/ThreadLoop").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace std::literals; using namespace std::chrono_literals; using Logger = TestSuite::Logger; - suite.addTest("isRunning", [&] { + suite.test("isRunning") = [&] { auto threadLoop = CppUtils::Thread::ThreadLoop{[] { std::this_thread::sleep_for(10ms); }}; - suite.expect(not threadLoop.isRunning()); + expect(not threadLoop.isRunning()); threadLoop.start(); - suite.expect(threadLoop.isRunning()); + expect(threadLoop.isRunning()); threadLoop.stop(); - suite.expect(not threadLoop.isRunning()); + expect(not threadLoop.isRunning()); threadLoop.start(); - suite.expect(threadLoop.isRunning()); - }); + expect(threadLoop.isRunning()); + }; - suite.addTest("Loop", [&] { + suite.test("Loop") = [&] { auto lockedNumber = CppUtils::Thread::UniqueLocker{0uz}; { auto threadLoop = CppUtils::Thread::ThreadLoop{[&lockedNumber] { @@ -33,14 +33,14 @@ namespace CppUtils::UnitTest::Thread::ThreadLoop ++nb; }}; threadLoop.start(); - suite.expectWait([&lockedNumber] { + expect([&lockedNumber] { return lockedNumber.access().value() >= 5uz; - }, 500ms, 5ms); + }) == eventually(500ms, 5ms); } auto accessor = lockedNumber.access(); auto nb = accessor.value(); - suite.expectEqual(nb, 5uz); - }); + expect(nb) == 5uz; + }; // Workaround on Windows (MSVC ABI) // https://github.com/llvm/llvm-project/issues/221609 @@ -61,7 +61,7 @@ namespace CppUtils::UnitTest::Thread::ThreadLoop using TestException = std::runtime_error; #endif - suite.addTest("onError callback", [&] { + suite.test("onError callback") = [&] { auto caughtMessage = CppUtils::Thread::UniqueLocker{""s}; auto count = 0uz; @@ -85,11 +85,11 @@ namespace CppUtils::UnitTest::Thread::ThreadLoop threadLoop.start(); std::this_thread::sleep_for(50ms); - suite.expect(threadLoop.isRunning()); + expect(threadLoop.isRunning()); threadLoop.stop(); - suite.expect(not threadLoop.isRunning()); + expect(not threadLoop.isRunning()); - suite.expectEqual(caughtMessage.access().value(), "Exception"); - }); - }}; + expect(caughtMessage.access().value()) == "Exception"; + }; + }; } diff --git a/tests/Thread/ThreadPool.mpp b/tests/Thread/ThreadPool.mpp index 720c4966..b096796d 100644 --- a/tests/Thread/ThreadPool.mpp +++ b/tests/Thread/ThreadPool.mpp @@ -5,24 +5,24 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::ThreadPool { - auto _ = TestSuite{"Thread/ThreadPool", {"UnitTest"}, [](TestSuite& suite) { + auto _ = testSuite("Thread/ThreadPool").dependsOn("UnitTest") = [](TestSuite& suite) { using namespace std::literals; using namespace std::chrono_literals; using Logger = TestSuite::Logger; - suite.addTest("0 task / 1 thread", [&] { + suite.test("0 task / 1 thread") = [&] { auto _ = CppUtils::Thread::ThreadPool{1}; - }); + }; - suite.addTest("0 task / 2 threads", [&] { + suite.test("0 task / 2 threads") = [&] { auto _ = CppUtils::Thread::ThreadPool{2}; - }); + }; - suite.addTest("0 task / N threads", [&] { + suite.test("0 task / N threads") = [&] { auto _ = CppUtils::Thread::ThreadPool{}; - }); + }; - suite.addTest("1 task with future.wait()", [&] { + suite.test("1 task with future.wait()") = [&] { auto called = std::atomic_bool{false}; { auto threadPool = CppUtils::Thread::ThreadPool{}; @@ -31,10 +31,10 @@ namespace CppUtils::UnitTest::Thread::ThreadPool }); future.wait(); } - suite.expect(called); - }); + expect(called); + }; - suite.addTest("waitUntilFinished", [&] { + suite.test("waitUntilFinished") = [&] { auto called = std::atomic_bool{false}; { auto threadPool = CppUtils::Thread::ThreadPool{}; @@ -43,12 +43,12 @@ namespace CppUtils::UnitTest::Thread::ThreadPool }); threadPool.waitUntilFinished(); } - suite.expect(called); - }); + expect(called); + }; - suite.addTest("Canary: LLVM #221609 std::runtime_error double-free", [&] { + suite.test("Canary: LLVM #221609 std::runtime_error double-free") = [] { #if defined(_WIN32) - suite.skip("Waiting for LLVM issue #221609 fix: https://github.com/llvm/llvm-project/issues/221609"); + skip("Waiting for LLVM issue #221609 fix: https://github.com/llvm/llvm-project/issues/221609"); #else auto exceptionPointer = std::exception_ptr{}; try @@ -66,10 +66,10 @@ namespace CppUtils::UnitTest::Thread::ThreadPool } catch (const std::exception& exception) { - suite.expectEqual(exception.what(), "Canary"sv); + expect(exception.what()) == "Canary"; } #endif - }); + }; // Workaround on Windows (MSVC ABI) // https://github.com/llvm/llvm-project/issues/221609 @@ -90,7 +90,7 @@ namespace CppUtils::UnitTest::Thread::ThreadPool using TestException = std::runtime_error; #endif - suite.addTest("Error handling with future", [&] { + suite.test("Error handling with future") = [&] { auto threadPool = CppUtils::Thread::ThreadPool{}; auto future = threadPool.call([] { throw TestException{"Test error"}; // throw std::runtime_error{"Test error"}; @@ -105,15 +105,15 @@ namespace CppUtils::UnitTest::Thread::ThreadPool { called = true; Logger::print<"error">("ThreadPool: Unhandled exception: {}", exception.what()); - suite.expectEqual(exception.what(), "Test error"sv); + expect(exception.what()) == "Test error"; } - suite.expect(called); - }); + expect(called); + }; - suite.addTest("Error handling without future", [&] { + suite.test("Error handling without future") = [&] { auto threadPool = CppUtils::Thread::ThreadPool{}; auto called = std::atomic_bool{false}; - threadPool.setOnError([&suite, &called](std::exception_ptr exceptionPointer) { + threadPool.setOnError([&called](std::exception_ptr exceptionPointer) { called = true; try { @@ -122,7 +122,7 @@ namespace CppUtils::UnitTest::Thread::ThreadPool catch (const std::exception& exception) { Logger::print<"error">("ThreadPool: Unhandled exception: {}", exception.what()); - suite.expectEqual(exception.what(), "Test error"sv); + expect(exception.what()) == "Test error"; } catch (...) { @@ -133,18 +133,18 @@ namespace CppUtils::UnitTest::Thread::ThreadPool throw TestException{"Test error"}; }); threadPool.waitUntilFinished(); - suite.expect(called); - }); + expect(called); + }; - suite.addTest("Task with return value", [&] { + suite.test("Task with return value") = [&] { auto threadPool = CppUtils::Thread::ThreadPool{}; auto future = threadPool.call([] { return 42; }); - suite.expectEqual(future.get(), 42); - }); + expect(future.get()) == 42; + }; - suite.addTest("Stress test", [&] { + suite.test("Stress test") = [&] { auto threadPool = CppUtils::Thread::ThreadPool{}; auto counter = std::atomic_size_t{0}; const auto numTasks = 10'000uz; @@ -159,7 +159,7 @@ namespace CppUtils::UnitTest::Thread::ThreadPool }); threadPool.waitUntilFinished(); - suite.expectEqual(counter.load(), numTasks); - }); - }}; + expect(counter.load()) == numTasks; + }; + }; } diff --git a/tests/Thread/TryAsync.mpp b/tests/Thread/TryAsync.mpp index 38f29552..d5249d29 100644 --- a/tests/Thread/TryAsync.mpp +++ b/tests/Thread/TryAsync.mpp @@ -5,44 +5,44 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::TryAsync { - auto _ = TestSuite{"Thread/TryAsync", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Thread/TryAsync").dependsOn("Logger") = [](TestSuite& suite) { using namespace std::literals; using Logger = TestSuite::Logger; - suite.addTest("Successful async call", [&] { + suite.test("Successful async call") = [&] { auto future = CppUtils::Thread::tryAsync([] { return 42; }); auto result = future.get(); - suite.expect(result); - suite.expectEqual(result.value(), 42); - }); + expect(result); + expect(result.value()) == 42; + }; - suite.addTest("Async call with exception", [&] { + suite.test("Async call with exception") = [&] { auto future = CppUtils::Thread::tryAsync([] { throw std::runtime_error{"Test exception"}; return 0; }); auto result = future.get(); - suite.expect(not result); - suite.expectEqual(result.error().what(), "Test exception"sv); - }); + expect(not result); + expect(result.error().what()) == "Test exception"; + }; - suite.addTest("Void async call", [&] { + suite.test("Void async call") = [&] { auto future = CppUtils::Thread::tryAsync([] { Logger::print("Void async call executed"); }); auto result = future.get(); - suite.expect(result); - }); + expect(result); + }; - suite.addTest("Void async call with exception", [&] { + suite.test("Void async call with exception") = [&] { auto future = CppUtils::Thread::tryAsync([] { throw std::runtime_error{"Void test exception"}; }); auto result = future.get(); - suite.expect(not result); - suite.expectEqual(result.error().what(), "Void test exception"sv); - }); - }}; + expect(not result); + expect(result.error().what()) == "Void test exception"; + }; + }; } diff --git a/tests/Thread/UniqueLocker.mpp b/tests/Thread/UniqueLocker.mpp index 1944d5ee..1b83a0a8 100644 --- a/tests/Thread/UniqueLocker.mpp +++ b/tests/Thread/UniqueLocker.mpp @@ -5,16 +5,16 @@ import CppUtils; namespace CppUtils::UnitTest::Thread::UniqueLocker { - auto _ = TestSuite{"Thread/UniqueLocker", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("Access", [&] { + auto _ = testSuite("Thread/UniqueLocker").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test("Access") = [&] { auto lockedString = CppUtils::Thread::UniqueLocker{"Test"}; { auto accessor = lockedString.access(); - suite.expectEqual(accessor.value(), "Test"); + expect(accessor.value()) == "Test"; } - }); + }; - suite.addTest("Test mutex", [&] { + suite.test("Test mutex") = [&] { auto lockedNumber = CppUtils::Thread::UniqueLocker{0uz}; auto incrementFunction = [&lockedNumber](std::size_t iterations) noexcept -> void { @@ -32,55 +32,55 @@ namespace CppUtils::UnitTest::Thread::UniqueLocker auto thread1 = std::jthread{incrementFunction, iterations}; } auto accessor = lockedNumber.access(); - suite.expectEqual(accessor.value(), iterations * 2); - }); + expect(accessor.value()) == iterations * 2; + }; - suite.addTest("Const access", [&] { + suite.test("Const access") = [&] { const auto constLockedString = CppUtils::Thread::UniqueLocker{"Test"}; auto accessor = constLockedString.access(); - suite.expectEqual(accessor.value(), "Test"); - }); + expect(accessor.value()) == "Test"; + }; - suite.addTest("Const& access", [&] { + suite.test("Const& access") = [&] { const auto& constLockedString = CppUtils::Thread::UniqueLocker{"Test"}; auto accessor = constLockedString.access(); - suite.expectEqual(accessor.value(), "Test"); - }); + expect(accessor.value()) == "Test"; + }; - suite.addTest("Const&& access", [&] { + suite.test("Const&& access") = [&] { const auto&& constLockedString = CppUtils::Thread::UniqueLocker{"Test"}; auto accessor = constLockedString.access(); - suite.expectEqual(accessor.value(), "Test"); - }); + expect(accessor.value()) == "Test"; + }; - suite.addTest("MultipleAccessor", [&] { + suite.test("MultipleAccessor") = [&] { auto lockedString1 = CppUtils::Thread::UniqueLocker{"Foo"}; auto lockedString2 = CppUtils::Thread::UniqueLocker{"Bar"}; { auto accessor = CppUtils::Thread::MultipleAccessor{lockedString1, lockedString2}; - suite.expectEqual(std::get<0>(accessor.values), "Foo"); - suite.expectEqual(std::get<1>(accessor.values), "Bar"); + expect(std::get<0>(accessor.values)) == "Foo"; + expect(std::get<1>(accessor.values)) == "Bar"; } - }); + }; - suite.addTest("Move constructor", [&] { + suite.test("Move constructor") = [&] { auto source = CppUtils::Thread::UniqueLocker>{std::make_unique(42)}; auto destination = CppUtils::Thread::UniqueLocker>{std::move(source)}; - suite.expect(source.access().value() == nullptr); - suite.expect(destination.access().value() != nullptr); - suite.expectEqual(*destination.access().value(), 42); - }); + expect(source.access().value()) == nullptr; + expect(destination.access().value()) != nullptr; + expect(*destination.access().value()) == 42; + }; - suite.addTest("Move assignment", [&] { + suite.test("Move assignment") = [&] { auto source = CppUtils::Thread::UniqueLocker>{std::make_unique(100)}; auto destination = CppUtils::Thread::UniqueLocker>{std::make_unique(200)}; destination = std::move(source); - suite.expect(source.access().value() == nullptr); - suite.expect(destination.access().value() != nullptr); - suite.expectEqual(*destination.access().value(), 100); - }); - }}; + expect(source.access().value()) == nullptr; + expect(destination.access().value()) != nullptr; + expect(*destination.access().value()) == 100; + }; + }; } diff --git a/tests/Type/Concept.mpp b/tests/Type/Concept.mpp index 483e30d8..fe99dae0 100644 --- a/tests/Type/Concept.mpp +++ b/tests/Type/Concept.mpp @@ -7,28 +7,67 @@ namespace CppUtils::UnitTest::Type::Concept { using namespace CppUtils::Type; - static_assert(not Printable); - static_assert(Printable); - static_assert(Printable); - static_assert(Printable); - static_assert(Printable); - static_assert(Printable); - static_assert(Printable); + constexpr auto isPathWithoutExact(const std::filesystem::path&) -> std::true_type { return {}; } + constexpr auto isPath(const Exact auto&) -> std::true_type { return {}; } + constexpr auto isPath(std::string_view) -> std::false_type { return {}; } - static_assert(not IsOneOf); - static_assert(not IsOneOf); - static_assert(IsOneOf); + auto _ = testSuite("Type/Concept").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test<[] { + static_assert(not Printable); + static_assert(Printable); + static_assert(Printable); + static_assert(Printable); + static_assert(Printable); + static_assert(Printable); + static_assert(Printable); + }>("Printable"); - static_assert(Specializes, std::vector>); - static_assert(not Specializes, std::map>); + suite.test<[] { + static_assert(not IsOneOf); + static_assert(not IsOneOf); + static_assert(IsOneOf); + }>("IsOneOf"); - constexpr auto isPathWithoutExact(const std::filesystem::path&) -> std::true_type { return {}; } + suite.test<[] { + static_assert(Specializes, std::vector>); + static_assert(not Specializes, std::map>); + }>("Specializes"); - static_assert(decltype(isPathWithoutExact("flow_name"))::value); + suite.test<[] { + static_assert(decltype(isPathWithoutExact("flow_name"))::value); + static_assert(decltype(isPath(std::declval()))::value); + static_assert(not decltype(isPath("flow_name"))::value); + }>("Exact"); - constexpr auto isPath(const Exact auto&) -> std::true_type { return {}; } - constexpr auto isPath(std::string_view) -> std::false_type { return {}; } + suite.test<[] { + static_assert(std::same_as, int>); + static_assert(std::same_as, int>); + static_assert(std::same_as, std::string>); + }>("UnderlyingElementType"); + + suite.test<[] { + static_assert(Character); + static_assert(Character); + static_assert(Character); + static_assert(Character); + static_assert(Character); + static_assert(Character); + static_assert(not Character); + static_assert(not Character); + static_assert(not Character); + }>("Character"); - static_assert(decltype(isPath(std::declval()))::value); - static_assert(not decltype(isPath("flow_name"))::value); + suite.test<[] { + static_assert(ArithmeticInteger); + static_assert(ArithmeticInteger); + static_assert(ArithmeticInteger); + static_assert(ArithmeticInteger); + static_assert(ArithmeticInteger); + static_assert(not ArithmeticInteger); + static_assert(not ArithmeticInteger); + static_assert(not ArithmeticInteger); + static_assert(not ArithmeticInteger); + static_assert(not ArithmeticInteger); + }>("ArithmeticInteger"); + }; } diff --git a/tests/Type/Enum.mpp b/tests/Type/Enum.mpp index 328fde20..28c79e36 100644 --- a/tests/Type/Enum.mpp +++ b/tests/Type/Enum.mpp @@ -49,21 +49,21 @@ namespace CppUtils::Type::Enum namespace CppUtils::UnitTest::Type::Enum { - auto _ = TestSuite{"Type/Enum", {"UnitTest"}, [](TestSuite& suite) { - suite.addTest("Basic", [&] { - suite.expectEqual(CppUtils::Type::Enum::toString(Enum1::A), "A1"); - suite.expectEqual(CppUtils::Type::Enum::toString(Enum1::B), "B1"); - suite.expectEqual(CppUtils::Type::Enum::toString(Enum1::C), "C1"); - }); + auto _ = testSuite("Type/Enum").dependsOn("UnitTest") = [](TestSuite& suite) { + suite.test("Basic") = [&] { + expect(CppUtils::Type::Enum::toString(Enum1::A)) == "A1"; + expect(CppUtils::Type::Enum::toString(Enum1::B)) == "B1"; + expect(CppUtils::Type::Enum::toString(Enum1::C)) == "C1"; + }; - suite.addTest("Missing value", [&] { - suite.expectEqual(CppUtils::Type::Enum::toString(Enum2::A), "A2"); - suite.expectEqual(CppUtils::Type::Enum::toString(Enum2::B), "Unknown"); - }); + suite.test("Missing value") = [&] { + expect(CppUtils::Type::Enum::toString(Enum2::A)) == "A2"; + expect(CppUtils::Type::Enum::toString(Enum2::B)) == "Unknown"; + }; - suite.addTest("Missing name", [&] { - suite.expectEqual(CppUtils::Type::Enum::toString(Enum3::A), "A3"); - suite.expectEqual(CppUtils::Type::Enum::toString(Enum3::B), "Unknown"); - }); - }}; + suite.test("Missing name") = [&] { + expect(CppUtils::Type::Enum::toString(Enum3::A)) == "A3"; + expect(CppUtils::Type::Enum::toString(Enum3::B)) == "Unknown"; + }; + }; } diff --git a/tests/Type/Mapping.mpp b/tests/Type/Mapping.mpp index 24712f4c..49a42d30 100644 --- a/tests/Type/Mapping.mpp +++ b/tests/Type/Mapping.mpp @@ -134,49 +134,49 @@ namespace CppUtils::UnitTest::Type::Mapping CppUtils::Type::Pair, CppUtils::Type::Pair>; - auto _ = TestSuite{"Type/Mapping/Runtime", {}, [](auto& suite) { - suite.addTest("Dense Enum (LookupTable)", [&] { - suite.expectEqual(DenseMap::toRhs(Dense::A, "Unknown"sv), "Alpha"sv); - suite.expectEqual(DenseMap::toRhs(Dense::B, "Unknown"sv), "Beta"sv); - suite.expectEqual(DenseMap::toLhs("Alpha"sv, Dense::C), Dense::A); - suite.expect(DenseMap::containsLhs(Dense::B)); - suite.expect(not DenseMap::containsLhs(static_cast(0))); - }); - - suite.addTest("Sparse Enum (Perfect Hash)", [&] { - suite.expectEqual(SparseMap::toRhs(Sparse::X, 0), 1); - suite.expectEqual(SparseMap::toRhs(Sparse::Z, 0), 3); - suite.expectEqual(SparseMap::toLhs(2, Sparse::X), Sparse::Y); - suite.expect(SparseMap::containsLhs(Sparse::Y)); - suite.expect(not SparseMap::containsLhs(static_cast(5))); - }); - - suite.addTest("String Mapping (Seed Search)", [&] { - suite.expectEqual(StringMap::toRhs("GET"sv, 0), 10); - suite.expectEqual(StringMap::toRhs("POST"sv, 0), 20); - suite.expectEqual(StringMap::toLhs(30, "UNKNOWN"sv), "DELETE"sv); - suite.expect(StringMap::containsLhs("GET"sv)); - suite.expect(not StringMap::containsLhs("PATCH"sv)); - }); - - suite.addTest("Custom Hashable (Member hash)", [&] { - suite.expectEqual(CustomMap::toRhs(CustomHashable{1}, "None"sv), "One"sv); - suite.expect(CustomMap::containsLhs(CustomHashable{2})); - suite.expect(not CustomMap::containsLhs(CustomHashable{3})); - }); - - suite.addTest("Homogeneity (O(1) Extraction)", [&] { + auto _ = testSuite("Type/Mapping/Runtime") = [](TestSuite& suite) { + suite.test("Dense Enum (LookupTable)") = [&] { + expect(DenseMap::toRhs(Dense::A, "Unknown"sv)) == "Alpha"; + expect(DenseMap::toRhs(Dense::B, "Unknown"sv)) == "Beta"; + expect(DenseMap::toLhs("Alpha"sv, Dense::C)) == Dense::A; + expect(DenseMap::containsLhs(Dense::B)); + expect(not DenseMap::containsLhs(static_cast(0))); + }; + + suite.test("Sparse Enum (Perfect Hash)") = [&] { + expect(SparseMap::toRhs(Sparse::X, 0)) == 1; + expect(SparseMap::toRhs(Sparse::Z, 0)) == 3; + expect(SparseMap::toLhs(2, Sparse::X)) == Sparse::Y; + expect(SparseMap::containsLhs(Sparse::Y)); + expect(not SparseMap::containsLhs(static_cast(5))); + }; + + suite.test("String Mapping (Seed Search)") = [&] { + expect(StringMap::toRhs("GET"sv, 0)) == 10; + expect(StringMap::toRhs("POST"sv, 0)) == 20; + expect(StringMap::toLhs(30, "UNKNOWN"sv)) == "DELETE"; + expect(StringMap::containsLhs("GET"sv)); + expect(not StringMap::containsLhs("PATCH"sv)); + }; + + suite.test("Custom Hashable (Member hash)") = [&] { + expect(CustomMap::toRhs(CustomHashable{1}, "None"sv)) == "One"; + expect(CustomMap::containsLhs(CustomHashable{2})); + expect(not CustomMap::containsLhs(CustomHashable{3})); + }; + + suite.test("Homogeneity (O(1) Extraction)") = [&] { using MethodMap = CppUtils::Type::Mapping< CppUtils::Type::Pair<1, CppUtils::String::Hasher{"GET"}>, CppUtils::Type::Pair<2, CppUtils::String::Hasher{"POST"}>>; - suite.expectEqual(MethodMap::toRhs(1, "UNKNOWN"sv), "GET"sv); - }); - - suite.addTest("No Default Value", [&] { - suite.expectEqual(StringMap::toRhs("GET"sv).value(), 10); - suite.expect(not StringMap::toRhs("UNKNOWN"sv)); - }); - }}; + expect(MethodMap::toRhs(1, "UNKNOWN"sv)) == "GET"; + }; + + suite.test("No Default Value") = [&] { + expect(StringMap::toRhs("GET"sv).value()) == 10; + expect(not StringMap::toRhs("UNKNOWN"sv)); + }; + }; } namespace ToStruct @@ -214,19 +214,19 @@ namespace CppUtils::UnitTest::Type::Mapping CppUtils::Type::Pair<"Email"_token, &EligibleLoyaltyMember::email>, CppUtils::Type::Pair<"Score"_token, &EligibleLoyaltyMember::loyaltyPoints>>; - auto _ = TestSuite{"Type/Mapping/toStruct", {}, [](auto& suite) { - suite.addTest("Direct member pointer mapping", [&] { + auto _ = testSuite("Type/Mapping/toStruct") = [](TestSuite& suite) { + suite.test("Direct member pointer mapping") = [&] { const auto user = User{"Alice", "alice@example.com", 150}; const auto member = CppUtils::Type::toStruct(user); - suite.expectEqual(member, EligibleLoyaltyMember{"Alice", "alice@example.com", 150}); - }); + expect(member) == EligibleLoyaltyMember{"Alice", "alice@example.com", 150}; + }; - suite.addTest("Token bridge mapping", [&] { + suite.test("Token bridge mapping") = [&] { const auto user = User{"Bob", "bob@example.com", 300}; const auto member = CppUtils::Type::toStruct(user); - suite.expectEqual(member, EligibleLoyaltyMember{"Bob", "bob@example.com", 300}); - }); - }}; + expect(member) == EligibleLoyaltyMember{"Bob", "bob@example.com", 300}; + }; + }; struct Point2D final { diff --git a/tests/Type/ReadWriteAdapter.mpp b/tests/Type/ReadWriteAdapter.mpp index 8890e7db..a57d802c 100644 --- a/tests/Type/ReadWriteAdapter.mpp +++ b/tests/Type/ReadWriteAdapter.mpp @@ -68,48 +68,48 @@ namespace CppUtils::UnitTest::Type::ReadWriteAdapter } }; - auto _ = TestSuite{"Type/ReadWriteAdapter", [](TestSuite& suite) { - suite.addTest("Generic Writer", [&suite] { + auto _ = testSuite("Type/ReadWriteAdapter") = [](TestSuite& suite) { + suite.test("Generic Writer") = [] { auto writer = CustomWriter{}; CppUtils::Type::Writer{}(writer, 1); - suite.expectEqual(writer.value, 1); - }); + expect(writer.value) == 1; + }; - suite.addTest("Generic Reader", [&suite] { + suite.test("Generic Reader") = [] { auto reader = CustomReader{}; { auto result = CppUtils::Type::Reader{}(reader); - suite.expectEqual(result, 1); + expect(result) == 1; } { auto value = 0; auto result = CppUtils::Type::Reader{}(reader, value); - suite.expect(result.has_value()); - suite.expectEqual(value, 1); + expect(result.has_value()); + expect(value) == 1; } - }); + }; - suite.addTest("Zero copy Reader", [&suite] { + suite.test("Zero copy Reader") = [] { auto zeroCopyReader = ZeroCopyReader{}; { auto value = 0; auto result = CppUtils::Type::Reader{}(zeroCopyReader, value); - suite.expect(result.has_value()); - suite.expectEqual(value, 2); + expect(result.has_value()); + expect(value) == 2; } { auto result = CppUtils::Type::Reader{}.template operator()(zeroCopyReader); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), 2); + expect(result.has_value()); + expect(result.value()) == 2; } - }); + }; - suite.addTest("std::ostringstream / std::istringstream specialization", [&suite] { + suite.test("std::ostringstream / std::istringstream specialization") = [] { auto expectedValue = 1; auto outputStream = std::ostringstream{std::ios::binary}; CppUtils::Type::Writer{}(outputStream, expectedValue); @@ -117,11 +117,11 @@ namespace CppUtils::UnitTest::Type::ReadWriteAdapter auto inputStream = std::istringstream{outputStream.str(), std::ios::binary}; auto value = 0; auto result = CppUtils::Type::Reader{}(inputStream, value); - suite.expect(result.has_value()); - suite.expectEqual(value, expectedValue); - }); + expect(result.has_value()); + expect(value) == expectedValue; + }; - suite.addTest("std::stringstream specialization", [&suite] { + suite.test("std::stringstream specialization") = [] { auto stream = std::stringstream{std::ios::binary | std::ios::in | std::ios::out}; auto inputValue = 1; @@ -130,58 +130,58 @@ namespace CppUtils::UnitTest::Type::ReadWriteAdapter { auto value = 0; auto result = CppUtils::Type::Reader{}(stream, value); - suite.expect(result.has_value()); - suite.expectEqual(value, inputValue); + expect(result.has_value()); + expect(value) == inputValue; } { stream.seekg(0); auto result = CppUtils::Type::Reader{}.template operator()(stream); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), inputValue); + expect(result.has_value()); + expect(result.value()) == inputValue; } - }); + }; - suite.addTest("std::vector and std::span specialization", [&suite] { + suite.test("std::vector and std::span specialization") = [] { auto buffer = std::vector{}; auto inputValue = 1; CppUtils::Type::write(buffer, inputValue); - suite.expectEqual(std::ranges::size(buffer), sizeof(inputValue)); + expect(std::ranges::size(buffer)) == sizeof(inputValue); auto view = std::span{buffer}; { auto value = 0; auto result = CppUtils::Type::read(view, value); - suite.expect(result.has_value()); - suite.expectEqual(value, inputValue); + expect(result.has_value()); + expect(value) == inputValue; } - suite.expectEqual(std::ranges::size(view), 0uz); - }); + expect(std::ranges::size(view)) == 0uz; + }; - suite.addTest("Customization Point Object (CPO) ADL", [&suite] { + suite.test("Customization Point Object (CPO) ADL") = [] { auto object = UserNamespace::CustomADLObject{}; CppUtils::Type::write(object, 1); - suite.expectEqual(object.writeValue, 1); + expect(object.writeValue) == 1; { auto value = 0; auto result = CppUtils::Type::read(object, value); - suite.expect(result.has_value()); - suite.expectEqual(value, 3); + expect(result.has_value()); + expect(value) == 3; } { auto errorObject = UserNamespace::CustomErrorADLObject{-1}; auto value = 0; auto result = CppUtils::Type::read(errorObject, value); - suite.expect(not result.has_value()); - suite.expectEqual(result.error(), -1); + expect(not result.has_value()); + expect(result.error()) == -1; } - }); + }; - suite.addTest("Reader errors", [&suite] { + suite.test("Reader errors") = [] { using namespace std::literals; auto buffer = std::vector{}; @@ -189,17 +189,17 @@ namespace CppUtils::UnitTest::Type::ReadWriteAdapter { auto value = 0; auto result = CppUtils::Type::read(view, value); - suite.expect(not result.has_value()); - suite.expectEqual(result.error(), "Reader: span out of range"sv); + expect(not result.has_value()); + expect(result.error()) == "Reader: span out of range"; } { auto inputStream = std::istringstream{"", std::ios::binary}; auto value = 0; auto result = CppUtils::Type::Reader{}(inputStream, value); - suite.expect(not result.has_value()); - suite.expectEqual(result.error(), "Reader: unexpected end of stream"sv); + expect(not result.has_value()); + expect(result.error()) == "Reader: unexpected end of stream"; } - }); - }}; + }; + }; } diff --git a/tests/Type/Serializer.mpp b/tests/Type/Serializer.mpp index ea1824a5..f2660792 100644 --- a/tests/Type/Serializer.mpp +++ b/tests/Type/Serializer.mpp @@ -52,90 +52,90 @@ namespace CppUtils::Type::Binary export namespace CppUtils::UnitTest::Type::Serializer { - auto _ = TestSuite{"Type/Serializer", [](TestSuite& suite) { + auto _ = testSuite("Type/Serializer") = [](TestSuite& suite) { using namespace std::literals; - suite.addTest("Trivial Type", [&suite] { + suite.test("Trivial Type") = [] { auto value = 42; auto buffer = CppUtils::Type::serialize(value); { auto view = std::span{buffer}; auto result = CppUtils::Type::deserialize(view); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), value); + expect(result.has_value()); + expect(result.value()) == value; } { auto view = std::span{buffer}; auto deserializedValue = 0; auto result = CppUtils::Type::deserialize(view, deserializedValue); - suite.expect(result.has_value()); - suite.expectEqual(deserializedValue, value); + expect(result.has_value()); + expect(deserializedValue) == value; } - }); + }; - suite.addTest("std::string", [&suite] { + suite.test("std::string") = [] { auto value = "Hello World!"s; auto buffer = CppUtils::Type::serialize(value); { auto view = std::span{buffer}; auto result = CppUtils::Type::deserialize(view); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), value); + expect(result.has_value()); + expect(result.value()) == value; } { auto view = std::span{buffer}; auto deserializedValue = ""s; auto result = CppUtils::Type::deserialize(view, deserializedValue); - suite.expect(result.has_value()); - suite.expectEqual(deserializedValue, value); + expect(result.has_value()); + expect(deserializedValue) == value; } - }); + }; - suite.addTest("std::vector", [&suite] { + suite.test("std::vector") = [] { auto value = std::vector{1, 2, 3, 4, 5}; auto buffer = CppUtils::Type::serialize(value); { auto view = std::span{buffer}; auto result = CppUtils::Type::deserialize>(view); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), value); + expect(result.has_value()); + expect(result.value()) == value; } { auto view = std::span{buffer}; auto deserializedValue = std::vector{}; auto result = CppUtils::Type::deserialize(view, deserializedValue); - suite.expect(result.has_value()); - suite.expectEqual(deserializedValue, value); + expect(result.has_value()); + expect(deserializedValue) == value; } - }); + }; - suite.addTest("Custom Type", [&suite] { + suite.test("Custom Type") = [] { auto value = CustomType{42, "Hello"}; auto buffer = CppUtils::Type::serialize(value); { auto view = std::span{buffer}; auto result = CppUtils::Type::deserialize(view); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), value); + expect(result.has_value()); + expect(result.value()) == value; } { auto view = std::span{buffer}; auto deserializedValue = CustomType{}; auto result = CppUtils::Type::deserialize(view, deserializedValue); - suite.expect(result.has_value()); - suite.expectEqual(deserializedValue, value); + expect(result.has_value()); + expect(deserializedValue) == value; } - }); + }; - suite.addTest("TextSerializer (Basic)", [&suite] { + suite.test("TextSerializer (Basic)") = [] { auto value = 42; auto buffer = ""s; CppUtils::Type::Text::Serializer::serialize(value, buffer); @@ -143,22 +143,22 @@ export namespace CppUtils::UnitTest::Type::Serializer { auto view = std::string_view{buffer}; auto result = CppUtils::Type::Text::Serializer::deserialize(view); - suite.expect(result.has_value()); - suite.expectEqual(result.value(), value); - suite.expectEqual(view, ""sv); + expect(result.has_value()); + expect(result.value()) == value; + expect(view) == ""; } { auto view = std::string_view{buffer}; auto deserializedValue = 0; auto result = CppUtils::Type::Text::Serializer::deserialize(view, deserializedValue); - suite.expect(result.has_value()); - suite.expectEqual(deserializedValue, value); - suite.expectEqual(view, ""sv); + expect(result.has_value()); + expect(deserializedValue) == value; + expect(view) == ""; } - }); + }; - suite.addTest("TextSerializer (Multiple)", [&suite] { + suite.test("TextSerializer (Multiple)") = [] { auto value0 = 3.14; auto value1 = 42; auto buffer = ""s; @@ -169,10 +169,10 @@ export namespace CppUtils::UnitTest::Type::Serializer auto result0 = CppUtils::Type::Text::Serializer::deserialize(view); auto result1 = CppUtils::Type::Text::Serializer::deserialize(view); - suite.expect(result0.has_value()); - suite.expect(result1.has_value()); - suite.expectEqual(result0.value(), value0); - suite.expectEqual(result1.value(), value1); - }); - }}; + expect(result0.has_value()); + expect(result1.has_value()); + expect(result0.value()) == value0; + expect(result1.value()) == value1; + }; + }; } diff --git a/tests/Type/Tuple.mpp b/tests/Type/Tuple.mpp index 27fbe38a..10ee2410 100644 --- a/tests/Type/Tuple.mpp +++ b/tests/Type/Tuple.mpp @@ -5,10 +5,10 @@ import CppUtils; namespace CppUtils::UnitTest::Type::Tuple { - auto _ = TestSuite{"Type/Tuple", {"Logger"}, [](TestSuite& suite) { + auto _ = testSuite("Type/Tuple").dependsOn("Logger") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("visitAt", [&] { + suite.test("visitAt") = [&] { auto called = std::atomic_bool{false}; auto function = [&called](auto&& value) { Logger::print<"detail">("{}", value); @@ -18,7 +18,7 @@ namespace CppUtils::UnitTest::Type::Tuple auto tuple = std::make_tuple(1, "Hello World!"); auto _ = CppUtils::Type::Tuple::visitAt(tuple, 1, function); - suite.expect(called); - }); - }}; + expect(called); + }; + }; } diff --git a/tests/UnitTest/JUnitFormatter.mpp b/tests/UnitTest/JUnitFormatter.mpp new file mode 100644 index 00000000..12e19605 --- /dev/null +++ b/tests/UnitTest/JUnitFormatter.mpp @@ -0,0 +1,47 @@ +export module CppUtils.UnitTests.UnitTest.JUnitFormatter; + +import std; +import CppUtils; + +namespace CppUtils::UnitTest::UnitTest::JUnitFormatter +{ + auto _ = testSuite("UnitTest/JUnitFormatter") = [](TestSuite& suite) { + using namespace std::chrono_literals; + + suite.test("JUnit::generateXml formats complete JUnit report with durations") = [&] { + const auto suites = std::vector{ + RecordedTestSuite{ + .name = "Math/Calculator", + .testCases = { + RecordedTestCase{ + .name = "Addition", + .className = "Math/Calculator", + .status = TestStatus::Success, + .duration = 10ms}, + RecordedTestCase{ + .name = "Division", + .className = "Math/Calculator", + .status = TestStatus::Failure, + .message = "Division by zero\nExpected not null", + .failureType = "TestException", + .duration = 5ms}, + RecordedTestCase{ + .name = "Multiplication", + .className = "Math/Calculator", + .status = TestStatus::Skipped, + .message = "Not implemented yet", + .duration = 0ms}}, + .duration = 15ms}}; + + const auto xml = JUnit::generateXml(suites, "MyTestSuiteRun", 20ms); + + expect(xml.contains(R"()")); + expect(xml.contains(R"(Division by zero)" "\n" R"(Expected not null)")); + expect(xml.contains(R"()")); + expect(xml.contains(R"()")); + }; + }; +} diff --git a/tests/UnitTest/LifetimeChecker.mpp b/tests/UnitTest/LifetimeChecker.mpp index 8d99a214..46a4ebf7 100644 --- a/tests/UnitTest/LifetimeChecker.mpp +++ b/tests/UnitTest/LifetimeChecker.mpp @@ -5,75 +5,75 @@ import CppUtils; namespace CppUtils::UnitTest::LifetimeCheckerTest { - auto _ = TestSuite{"UnitTest/LifetimeChecker", [](TestSuite& suite) { + auto _ = testSuite("UnitTest/LifetimeChecker") = [](TestSuite& suite) { using Logger = TestSuite::Logger; - suite.addTest("No Copy No Move", [&] { + suite.test("No Copy No Move") = [&] { auto object = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "LifetimeChecker"}; - suite.expectEqual(object.getCopyCount(), 0uz); - suite.expectEqual(object.getMoveCount(), 0uz); - }); + expect(object.getCopyCount()) == 0uz; + expect(object.getMoveCount()) == 0uz; + }; - suite.addTest("Copy Construction", [&] { + suite.test("Copy Construction") = [&] { auto object = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "LifetimeChecker"}; auto copied = object; - suite.expectEqual(copied.getCopyCount(), 1uz); - suite.expectEqual(copied.getMoveCount(), 0uz); - }); + expect(copied.getCopyCount()) == 1uz; + expect(copied.getMoveCount()) == 0uz; + }; - suite.addTest("Move Construction", [&] { + suite.test("Move Construction") = [&] { auto object = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "LifetimeChecker"}; auto moved = std::move(object); - suite.expectEqual(moved.getCopyCount(), 0uz); - suite.expectEqual(moved.getMoveCount(), 1uz); - }); + expect(moved.getCopyCount()) == 0uz; + expect(moved.getMoveCount()) == 1uz; + }; - suite.addTest("Copy Assignment", [&] { + suite.test("Copy Assignment") = [&] { auto object = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "LifetimeChecker"}; auto target = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "Target"}; target = object; - suite.expectEqual(target.getCopyCount(), 1uz); - suite.expectEqual(target.getMoveCount(), 0uz); - }); + expect(target.getCopyCount()) == 1uz; + expect(target.getMoveCount()) == 0uz; + }; - suite.addTest("Move Assignment", [&] { + suite.test("Move Assignment") = [&] { auto object = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "LifetimeChecker"}; auto target = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "Target"}; target = std::move(object); - suite.expectEqual(target.getCopyCount(), 0uz); - suite.expectEqual(target.getMoveCount(), 1uz); - }); + expect(target.getCopyCount()) == 0uz; + expect(target.getMoveCount()) == 1uz; + }; - suite.addTest("Two Copies", [&] { + suite.test("Two Copies") = [&] { auto object = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "LifetimeChecker"}; auto copy1 = object; auto copy2 = copy1; - suite.expectEqual(copy2.getCopyCount(), 2uz); - suite.expectEqual(copy2.getMoveCount(), 0uz); - }); + expect(copy2.getCopyCount()) == 2uz; + expect(copy2.getMoveCount()) == 0uz; + }; - suite.addTest("Two Moves", [&] { + suite.test("Two Moves") = [&] { auto object = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "LifetimeChecker"}; auto moved1 = std::move(object); auto moved2 = std::move(moved1); - suite.expectEqual(moved2.getCopyCount(), 0uz); - suite.expectEqual(moved2.getMoveCount(), 2uz); - }); + expect(moved2.getCopyCount()) == 0uz; + expect(moved2.getMoveCount()) == 2uz; + }; - suite.addTest("Copy then Move", [&] { + suite.test("Copy then Move") = [&] { auto object = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "LifetimeChecker"}; auto copy = object; auto moved = std::move(copy); - suite.expectEqual(moved.getCopyCount(), 1uz); - suite.expectEqual(moved.getMoveCount(), 1uz); - }); + expect(moved.getCopyCount()) == 1uz; + expect(moved.getMoveCount()) == 1uz; + }; - suite.addTest("Move then Copy", [&] { + suite.test("Move then Copy") = [&] { auto object = LifetimeChecker{[](std::string_view message) { Logger::print<"info">("{}", message); }, "LifetimeChecker"}; auto moved = std::move(object); auto copy = moved; - suite.expectEqual(copy.getCopyCount(), 1uz); - suite.expectEqual(copy.getMoveCount(), 1uz); - }); - }}; + expect(copy.getCopyCount()) == 1uz; + expect(copy.getMoveCount()) == 1uz; + }; + }; } diff --git a/tests/UnitTest/UnitTest.mpp b/tests/UnitTest/UnitTest.mpp index b620f4f1..4364d2a4 100644 --- a/tests/UnitTest/UnitTest.mpp +++ b/tests/UnitTest/UnitTest.mpp @@ -5,42 +5,712 @@ import CppUtils; namespace CppUtils::UnitTest::UnitTest { - auto _ = TestSuite{"UnitTest", [](TestSuite& suite) { - suite.addTest("empty test", [] {}); + auto _ = testSuite("UnitTest") = [](TestSuite& suite) { + using namespace std::literals; + using namespace std::chrono_literals; - suite.addTest("expect", [&] { - suite.expect(true); - }); + suite.test("empty test") = [] {}; - suite.addTest("expectEqual", [&] { - using namespace std::literals; + suite.test("expect") = [&] { + expect(true); + }; - suite.expectEqual(1, 1); - suite.expectEqual("test"sv, "test"sv); - }); + suite.test("Comparison assertions with standard types") = [&] { + const auto leftNumber = 42; + const auto rightNumber = 42; + expect(leftNumber) == rightNumber; - suite.addTest("expectWait", [&] { - using namespace std::chrono_literals; + expect(10) > 5; + expect(10) >= 10; + expect(5) < 10; + expect(5) <= 5; + expect(1) != 2; + + expect(0.1 + 0.2) == 0.3; + expect(0.1 + 0.2) != 0.4; + expect(0.1 + 0.2) <= 0.3; + expect(0.1 + 0.2) >= 0.3; + + expect("hello") == "hello"; + expect("admin"s) == "admin"; + + expect(10ms) == 10ms; + expect(1s) == 1000ms; + expect(500us) > 100ns; + expect(1min) == 60s; + expect(1h) == 60min; + + expect('A') == 'A'; + expect('Z') > 'A'; + expect(L'A') == L'A'; + + expect(std::byte{0x30}) == 0x30; + expect(std::byte{0x30}) == std::byte{0x30}; + + try + { + expect(1) == 2; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In expect(lhs) == rhs")); + expect(message.contains("with lhs:\n1")); + expect(message.contains("and rhs:\n2")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on ordering") = [&] { + try + { + expect(10) < 5; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In expect(lhs) < rhs")); + expect(message.contains("with lhs:\n10")); + expect(message.contains("and rhs:\n5")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on duration comparison") = [&] { + try + { + expect(10ms) < 5ms; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In expect(lhs) < rhs")); + expect(message.contains("with lhs:\n10ms")); + expect(message.contains("and rhs:\n5ms")); + return; + } + + expect(false); + }; + + suite.test("Expression capture with printable ranges") = [&] { + const auto actualHeader = std::vector{"header1", "header2"}; + const auto expectedHeader = std::vector{"header1", "header2"}; + expect(actualHeader) == expectedHeader; + + const auto numbers = std::vector{1, 2, 3}; + expect(numbers) == std::vector{1, 2, 3}; + + try + { + expect(actualHeader) == std::vector{"other1", "other2"}; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains(R"(with lhs: +["header1", "header2"])")); + expect(message.contains(R"(and rhs: +["other1", "other2"])")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on std::ranges::size comparison") = [&] { + const auto users = std::vector{"alice", "bob"}; + expect(std::ranges::size(users)) == 2uz; + + try + { + expect(std::ranges::size(users)) == 3uz; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In expect(lhs) == rhs")); + expect(message.contains("with lhs:\n2")); + expect(message.contains("and rhs:\n3")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on tuple comparison") = [&] { + const auto actualTuple = std::tuple{404, "Not Found"}; + const auto expectedTuple = std::tuple{200, "OK"}; + + try + { + expect(actualTuple) == expectedTuple; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains(R"(with lhs: +(404, "Not Found"))")); + expect(message.contains(R"(and rhs: +(200, "OK"))")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on pair comparison") = [&] { + const auto actualPair = std::pair{404, "Not Found"}; + const auto expectedPair = std::pair{200, "OK"}; + + try + { + expect(actualPair) == expectedPair; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains(R"(with lhs: +(404, "Not Found"))")); + expect(message.contains(R"(and rhs: +(200, "OK"))")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on filesystem path comparison") = [&] { + const auto actualPath = std::filesystem::path{"/var/log/syslog"}; + const auto expectedPath = std::filesystem::path{"/var/log/app.log"}; + + try + { + expect(actualPath) == expectedPath; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("with lhs:\n/var/log/syslog")); + expect(message.contains("and rhs:\n/var/log/app.log")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on std::byte comparison") = [&] { + const auto actualByte = std::byte{0x2A}; + const auto expectedByte = std::byte{0xFF}; + + try + { + expect(actualByte) == expectedByte; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("with lhs:\n0x2A")); + expect(message.contains("and rhs:\n0xFF")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on standalone expect boolean") = [&] { + try + { + expect(false); + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In expect(condition)")); + expect(message.contains("with value:")); + expect(message.contains(Terminal::TextColor::Ansi::EscapeCode::Red)); + expect(message.contains("false")); + return; + } + + expect(false); + }; + + suite.test("not expect(condition)") = [&] { + not expect(false); + + try + { + not expect(true); + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In not expect(condition)")); + expect(message.contains("with value:")); + expect(message.contains(Terminal::TextColor::Ansi::EscapeCode::Red)); + expect(message.contains("true")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on empty optional condition") = [&] { + const auto emptyOptional = std::optional{}; + try + { + expect(emptyOptional); + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In expect(condition)")); + expect(message.contains("with value:")); + expect(message.contains(Terminal::TextColor::Ansi::EscapeCode::Red)); + expect(message.contains("nullopt")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on dereferenceable optional negation") = [&] { + const auto populatedOptional = std::optional{"Connexion refusée"}; + try + { + not expect(populatedOptional); + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In not expect(condition)")); + expect(message.contains("with value:")); + expect(message.contains(Terminal::TextColor::Ansi::EscapeCode::Red)); + expect(message.contains("Connexion refusée")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on dereferenceable custom wrapper negation") = [&] { + struct CustomPayload final + { + std::string payload; + explicit operator bool() const noexcept { return not std::ranges::empty(payload); } + [[nodiscard]] auto operator*() const noexcept -> const std::string& { return payload; } + }; + + const auto customObject = CustomPayload{"Erreur applicative personnalisée"}; + try + { + not expect(customObject); + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In not expect(condition)")); + expect(message.contains("with value:")); + expect(message.contains(Terminal::TextColor::Ansi::EscapeCode::Red)); + expect(message.contains("Erreur applicative personnalisée")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on null pointer condition") = [&] { + const auto nullPointer = std::unique_ptr{}; + try + { + expect(nullPointer); + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In expect(condition)")); + expect(message.contains("with value:")); + expect(message.contains(Terminal::TextColor::Ansi::EscapeCode::Red)); + expect(message.contains("nullptr")); + return; + } + + expect(false); + }; + + suite.test("Failure diagnostics on valid pointer negation") = [&] { + const auto validPointer = std::make_unique(42); + try + { + not expect(validPointer); + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("In not expect(condition)")); + expect(message.contains("with value:")); + expect(message.contains(Terminal::TextColor::Ansi::EscapeCode::Red)); + expect(message.contains("42")); + return; + } + + expect(false); + }; + + suite.test<[] { + return 2 + 2 == 4; + }>("Compile-time test validation"); + + suite.test<[] { + auto fuzzer = CppUtils::Math::DeterministicRandomEngine{}; + for (auto i = 0uz; i < 50uz; ++i) + { + const auto a = fuzzer.random(1, 100); + const auto b = fuzzer.random(1, 100); + if ((a / b) * b + (a % b) != a) + return false; + } + const auto floatValue = fuzzer.random(); + if (floatValue < 0.0 or floatValue > 1.0) + return false; + const auto sizeValue = fuzzer.random(10uz, 100uz); + if (sizeValue < 10uz or sizeValue > 100uz) + return false; + const auto boolValue = fuzzer.random(); + static_assert(std::same_as); + return true; + }>("Compile-time deterministic fuzzing"); - suite.expectWait([] { + suite.test("eventually matcher") = [&] { + expect([] { return true; - }); + }) == eventually; auto counter = 0uz; - suite.expectWait([&] { + expect([&] { return ++counter >= 3uz; - }, 200ms, 10ms); + }) == eventually(200ms, 10ms); - suite.expectThrow([&] { - suite.expectWait([] { + auto ready = false; + auto thread = std::jthread{[&ready] { + std::this_thread::sleep_for(20ms); + ready = true; + }}; + expect([&ready] { + return ready; + }) == eventually(500ms); + + expect([&] { + expect([] { return false; - }, 30ms, 10ms); + }) == eventually(30ms, 10ms); + }) == throws; + }; + + suite.test("completes matcher on future") = [] { + auto promise = std::promise{}; + auto future = promise.get_future(); + promise.set_value(); + + expect(future) == completes; + }; + + suite.test("completes.with matcher on future") = [] { + auto promise = std::promise{}; + auto future = promise.get_future(); + promise.set_value(42); + + expect(future) == completes.with(42); + }; + + suite.test("completes.with matcher value mismatch failure") = [] { + auto promise = std::promise{}; + auto future = promise.get_future(); + promise.set_value(42); + + expect([&] { + expect(future) == completes.with(999); + }) == throws; + }; + + suite.test("completes matcher with custom timeout") = [] { + auto promise = std::promise{}; + auto future = promise.get_future(); + promise.set_value(); + + expect(future) == completes(500ms); + }; + + suite.test("completes.with matcher with custom timeout") = [] { + auto promise = std::promise{}; + auto future = promise.get_future(); + promise.set_value(100); + + expect(future) == completes(500ms).with(100); + }; + + suite.test("completes matcher on future timeout failure") = [] { + auto unfulfilledPromise = std::promise{}; + auto unfulfilledFuture = unfulfilledPromise.get_future(); + + expect([&] { + expect(unfulfilledFuture) == completes(20ms); + }) == throws; + + // Workaround on Windows (MSVC ABI) + // https://github.com/llvm/llvm-project/issues/221609 + // Abandoning an unfulfilled std::promise allocates an internal broken_promise exception + // whose destruction triggers heap corruption under Clang with MSVC ABI. + unfulfilledPromise.set_value(); + }; + + suite.test("completes matcher on binary semaphore") = [] { + auto semaphore = std::binary_semaphore{0}; + semaphore.release(); + + expect(semaphore) == completes; + }; + + suite.test("completes matcher on binary semaphore timeout failure") = [] { + auto unreleasedSemaphore = std::binary_semaphore{0}; + expect([&] { + expect(unreleasedSemaphore) == completes(20ms); + }) == throws; + }; + + suite.test("completes matcher on callable returning future") = [] { + expect([] { + auto promise = std::promise{}; + auto future = promise.get_future(); + promise.set_value(); + return future; + }) == completes; + }; + + suite.test("completes.with matcher on callable returning future") = [] { + expect([] { + auto promise = std::promise{}; + auto future = promise.get_future(); + promise.set_value(42); + return future; + }) == completes.with(42); + }; + + suite.test("completes matcher on shared_future") = [] { + auto promise = std::promise{}; + auto sharedFuture = promise.get_future().share(); + promise.set_value(42); + + expect(sharedFuture) == completes; + expect(sharedFuture) == completes.with(42); + }; + + suite.test("completes matcher on counting semaphore") = [] { + auto countingSemaphore = std::counting_semaphore<8>{0}; + countingSemaphore.release(2); + + expect(countingSemaphore) == completes; + }; + + suite.test("crashes matcher") = [] { +#if defined(_WIN32) + skip("crashes matcher is not supported on Windows (POSIX fork/signals required)"); +#else + expect([] { + std::abort(); + }) == crashes; + + expect([] { + std::abort(); + }) == crashes(CppUtils::System::Signal::Abort); +#endif + }; + + suite.test("throws matcher with type") = [&] { + expect([] { + throw std::runtime_error{"Fatal error"}; + }) == throws; + }; + + suite.test("throws matcher with expected message") = [&] { + expect([] { + throw std::invalid_argument{"Division par zéro"}; + }) == throws("Division par zéro"); + }; + + suite.test("throws matcher with predicate") = [&] { + expect([] { + throw std::invalid_argument{"Division par zéro"}; + }) == throws([](const std::invalid_argument& error) { + return std::string_view{error.what()}.contains("zéro"); }); - }); + }; + + suite.test("throws matcher with custom value equality") = [&] { + struct CustomError final + { + int code; + std::string message; + + bool operator==(const CustomError& other) const = default; + }; + + expect([] { + throw CustomError{.code = 404, .message = "Not found"}; + }) == throws(CustomError{.code = 404, .message = "Not found"}); + }; + + suite.test("noThrow matcher") = [&] { + expect([] { + auto _ = 1 + 1; + }) == noThrow; + }; + + suite.test("throws matcher fails when no exception is thrown") = [] { + try + { + expect([] {}) == throws; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("Expected exception of type")); + expect(message.contains("no exception was thrown")); + return; + } + fail("Expected TestException was not thrown when no exception occurred"); + }; - suite.addTest("Canary: LLVM #221609 std::throw_with_nested double-free", [&] { + suite.test("throws matcher fails when a different exception is thrown") = [] { + try + { + expect([] { throw std::logic_error{"Wrong"}; }) == throws; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("caught different exception")); + return; + } + fail("Expected TestException was not thrown when a different exception occurred"); + }; + + suite.test("throws matcher fails when exception message does not match") = [] { + try + { + expect([] { throw std::invalid_argument{"Actual message"}; }) == throws("Expected message"); + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("message did not contain expected text")); + expect(message.contains("Expected: \"Expected message\"")); + expect(message.contains("Actual: \"Actual message\"")); + return; + } + fail("Expected TestException was not thrown when message did not match"); + }; + + suite.test("noThrow matcher fails when an unexpected exception is thrown") = [] { + try + { + expect([] { throw std::runtime_error{"Surprise"}; }) == noThrow; + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("Expected no throw, but an exception was thrown")); + return; + } + fail("Expected TestException was not thrown when unexpected exception occurred in noThrow"); + }; + + suite.test("eventually matcher fails when predicate times out") = [] { + try + { + expect([] { return false; }) == eventually(50ms, 10ms); + } + catch (const TestException& exception) + { + const auto message = std::string_view{exception.what()}; + expect(message.contains("Timed out waiting for predicate to become true")); + return; + } + fail("Expected TestException was not thrown when eventually predicate timed out"); + }; + + suite.test("EventWatcher with EventQueue") = [] { + auto queue = CppUtils::Execution::EventQueue{}; + + { + auto watcher = watch<"Message", int>(queue); + expect(not watcher.hasOccurred()); + + expect(not watcher.hasOccurred()); + expect(watcher.count()) == 0uz; + + queue.emit<"Message">(100); + queue.emit<"Message">(200); + + expect(watcher.waitForOccurrences(2uz, 1000ms)); + expect(watcher.hasOccurred()); + auto occurrences = watcher.occurrences(); + expect(occurrences->count) == 2uz; + expect(std::ranges::size(occurrences->payloads)) == 2uz; + expect(occurrences->payloads[0]) == 100; + expect(occurrences->payloads.back()) == 200; + } + + { + auto voidWatcher = watch<"VoidMessage">(queue); + expect(not voidWatcher.hasOccurred()); + + queue.emit<"VoidMessage">(); + + expect(voidWatcher.waitForEvent(1000ms)); + expect(voidWatcher.count()) == 1uz; + expect(voidWatcher.occurrences()->count) == 1uz; + expect(std::ranges::empty(voidWatcher.occurrences()->payloads)); + } + }; + + suite.test("Parameterized test with forTypes").forTypes() = [&] { + auto instance = Type{}; + expect(instance) == Type{}; + }; + + suite.test("Parameterized test with forValues").forValues(10, 20, 30) = [&](int value) { + expect(value) > 0; + }; + + suite.test("Parameterized test with range forValues").forValues(std::array{100, 200}) = [&](int number) { + expect(number) >= 100; + }; + + suite.test("Parameterized test with map forValues").forValues(std::map{{"admin", 4}, {"user", 2}}) = [&](const auto& entry) { + const auto& [role, permission] = entry; + expect(permission) > 0; + }; + + suite.fuzzTest("Deterministic fuzzing with random", 100) = [&](auto& fuzzer) { + const auto number = fuzzer.random(10, 50); + expect(number >= 10 and number <= 50); + + const auto sizeValue = fuzzer.random(5uz, 25uz); + expect(sizeValue >= 5uz and sizeValue <= 25uz); + + const auto uint32Value = fuzzer.template random(); + Execution::doNotOptimize(uint32Value); + }; + + suite.test("Canary: LLVM #221609 std::throw_with_nested double-free") = [] { #if defined(_WIN32) - suite.skip("Waiting for LLVM issue #221609 fix: https://github.com/llvm/llvm-project/issues/221609"); + skip("Waiting for LLVM issue #221609 fix: https://github.com/llvm/llvm-project/issues/221609"); #else using namespace std::literals; auto caughtMessage = std::string{}; @@ -59,8 +729,93 @@ namespace CppUtils::UnitTest::UnitTest { caughtMessage = exception.what(); } - suite.expectEqual(caughtMessage, "Canary outer"s); + expect(caughtMessage) == "Canary outer"; #endif - }); - }}; + }; + + suite.test("Suite name initialization") = [&] { + const auto explicitSuite = CppUtils::UnitTest::testSuite("CustomName"); + expect(explicitSuite.name) == "CustomName"; + }; + + suite.test("CLI parameters parsing") = [&] { + const auto arguments = std::array{"--filter=Math", "--fail-fast", "--report=junit:build/custom.xml", "-c"}; + const auto settings = CppUtils::UnitTest::executeCommands(arguments); + expect(settings.filter) == "Math"; + expect(settings.failFast) == true; + expect(settings.report) == "junit:build/custom.xml"; + expect(settings.chrono) == true; + + const auto shortArguments = std::array{"-f=String", "-r=out.xml", "-v=0"}; + const auto shortSettings = CppUtils::UnitTest::executeCommands(shortArguments); + expect(shortSettings.filter) == "String"; + expect(shortSettings.report) == "out.xml"; + expect(shortSettings.verbose) == false; + }; + + suite.test("Visual colored diff on expectation failure") = [&] { + auto exceptionMessage = std::string{}; + try + { + const auto expected = std::string{"hello world"}; + const auto actual = std::string{"hello word"}; + expect(expected) == actual; + } + catch (const TestException& exception) + { + exceptionMessage = exception.what(); + } + expect(exceptionMessage.contains("Diff:")) == true; + expect(exceptionMessage.contains("lhs (-):")) == true; + expect(exceptionMessage.contains("rhs (+):")) == true; + }; + + suite.test("JUnit XML report export") = [&] { + using namespace std::chrono_literals; + FileSystem::TemporaryDirectory{[&](const auto& directory) { + const auto temporaryReportPath = directory / "test-report-sample.xml"; + + auto recordedCase = RecordedTestCase{ + .name = "sampleTest", + .className = "SampleSuite", + .status = TestStatus::Success, + .message = {}, + .duration = 5ms}; + auto recordedSuite = RecordedTestSuite{ + .name = "SampleSuite", + .testCases = {std::move(recordedCase)}, + .duration = 5ms}; + + const auto suites = std::array{std::move(recordedSuite)}; + JUnit::writeJUnitXmlReport(temporaryReportPath, suites, 5ms); + + expect(std::filesystem::exists(temporaryReportPath)) == true; + auto file = std::ifstream{temporaryReportPath}; + auto content = std::string{std::istreambuf_iterator{file}, std::istreambuf_iterator{}}; + expect(content.contains(R"()")) == true; + expect(content.contains(R"(() = [](TestSuite& suite) { + suite.test("Instantiation") = [&] { + auto instance = Type{}; + expect(instance) == Type{}; + }; + }; + } + + namespace ParameterizedValues + { + auto _ = testSuite("UnitTest/Parameterized suite with forValues").forValues(5, 15) = [](TestSuite& suite, int value) { + suite.test("Value check") = [value] { + expect(value) > 0; + }; + }; + } } diff --git a/tests/UnitTests.mpp b/tests/UnitTests.mpp index bdbe4a04..fd00fca2 100644 --- a/tests/UnitTests.mpp +++ b/tests/UnitTests.mpp @@ -54,6 +54,7 @@ export import CppUtils.UnitTests.Language.Markdown.MarkdownLexer; export import CppUtils.UnitTests.Language.Xml.Pipeline; export import CppUtils.UnitTests.Language.Xml.XmlLexer; export import CppUtils.UnitTests.Language.Xml.FlowRunner; +export import CppUtils.UnitTests.Language.Xml.Utility; export import CppUtils.UnitTests.Language.VirtualMachine; export import CppUtils.UnitTests.Log.FileSink; export import CppUtils.UnitTests.LogRotate; @@ -73,9 +74,13 @@ export import CppUtils.UnitTests.Ranges.Expected; export import CppUtils.UnitTests.Ranges.Inspect; export import CppUtils.UnitTests.Ranges.Parallel; export import CppUtils.UnitTests.Stl.Format; +export import CppUtils.UnitTests.String.Concept; export import CppUtils.UnitTests.String.Encoding; export import CppUtils.UnitTests.String.Utility; export import CppUtils.UnitTests.System.Error; +export import CppUtils.UnitTests.System.Pipe; +export import CppUtils.UnitTests.System.Process; +export import CppUtils.UnitTests.System.Sandbox; export import CppUtils.UnitTests.Terminal; export import CppUtils.UnitTests.Terminal.AutoScroll; export import CppUtils.UnitTests.Terminal.Button; @@ -108,3 +113,4 @@ export import CppUtils.UnitTests.Type.Tuple; export import CppUtils.UnitTests.Type.TypeMapping; export import CppUtils.UnitTests.UnitTest; export import CppUtils.UnitTests.UnitTest.LifetimeChecker; +export import CppUtils.UnitTests.UnitTest.JUnitFormatter; diff --git a/tests/main.cpp b/tests/main.cpp index bfb533d6..7746eb3b 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -1,17 +1,15 @@ import std; import CppUtils; -import CppUtils.UnitTests; -auto start([[maybe_unused]] std::span arguments) -> int +auto start(std::span arguments) -> int { try { - /* - auto& settings = CppUtils::UnitTest::executeCommands(argc, argv); + if (not std::ranges::empty(arguments)) + arguments = arguments.subspan(1); + auto settings = CppUtils::UnitTest::executeCommands(arguments); if (settings.abort) return CppUtils::exitSuccess; - */ - auto settings = CppUtils::UnitTest::TestSettings{.fastAbort = false}; return CppUtils::UnitTest::executeTests(std::move(settings)); } catch (const std::exception& exception)