From 7050ffbecc81dff9982a4823e90581828bd6f313 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 01:05:28 +0000 Subject: [PATCH 1/3] Initial plan From 1bbb30bce80eac82b511a9b2fd591e0537ac124f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 01:12:28 +0000 Subject: [PATCH 2/3] Implement Pluralize tests and add checkboxes to TESTS_COVERAGE.md Co-authored-by: qizh <941415+qizh@users.noreply.github.com> --- Docs/TESTS_COVERAGE.md | 82 ++++++------- Tests/QizhKitTests/PluralizeTests.swift | 152 ++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 41 deletions(-) create mode 100644 Tests/QizhKitTests/PluralizeTests.swift diff --git a/Docs/TESTS_COVERAGE.md b/Docs/TESTS_COVERAGE.md index 9960730..ad8ff1f 100644 --- a/Docs/TESTS_COVERAGE.md +++ b/Docs/TESTS_COVERAGE.md @@ -2,7 +2,7 @@ This document tracks unit-test candidates discovered while scanning the QizhKit codebase, grouped by source area. Each entry lists the public APIs worth covering and concrete test ideas to validate their behavior. -## ⊞ [Components/Airtable/AirtableFormulaBuilder.swift](Components/Airtable/AirtableFormulaBuilder.swift) +## - [ ] ⊞ [Components/Airtable/AirtableFormulaBuilder.swift](Components/Airtable/AirtableFormulaBuilder.swift) @@ -24,7 +24,7 @@ This document tracks unit-test candidates discovered while scanning the QizhKit
-`testProducesAirtableFriendlyStrings` +- [ ] `testProducesAirtableFriendlyStrings` `.equals`, `.notEquals`, `.isEmpty`, and `.id` @@ -35,7 +35,7 @@ Produce Airtable-friendly strings
-`testCombinesFormulasWithAndOrNot` +- [ ] `testCombinesFormulasWithAndOrNot` `.and`, `.or`, `.not` @@ -46,7 +46,7 @@ Validate `.and`, `.or`, and `.not` nest descriptions correctly for multiple chil
-`testEscapesApostrophesInInterpolation` +- [ ] `testEscapesApostrophesInInterpolation` `appendInterpolation(_:)`, `withApostrophesEscaped` @@ -60,7 +60,7 @@ Verify the custom string interpolation paths escape single quotes consistently f
-## ⊞ [Components/Random Generators/SeededRandomGenerator.swift](Components/Random%20Generators/SeededRandomGenerator.swift) +## - [ ] ⊞ [Components/Random Generators/SeededRandomGenerator.swift](Components/Random%20Generators/SeededRandomGenerator.swift) @@ -80,7 +80,7 @@ Verify the custom string interpolation paths escape single quotes consistently f
-`testProducesRepeatableSequence` +- [ ] `testProducesRepeatableSequence` Confirm identical seeds emit identical sequences across multiple draws @@ -88,7 +88,7 @@ Confirm identical seeds emit identical sequences across multiple draws
-`testMixes64BitOutput` +- [ ] `testMixes64BitOutput` Assert two 32-bit GK samples are combined into varying high/low bits to prevent bias @@ -96,7 +96,7 @@ Assert two 32-bit GK samples are combined into varying high/low bits to prevent
-`testAdvancesStateBetweenCalls` +- [ ] `testAdvancesStateBetweenCalls` Ensure successive `next()` calls mutate generator state (no repeated constant) @@ -107,7 +107,7 @@ Ensure successive `next()` calls mutate generator state (no repeated constant)
-## ⊞ [Extensions/String+/String+modify.swift](Extensions/String+/String+modify.swift) +## - [ ] ⊞ [Extensions/String+/String+modify.swift](Extensions/String+/String+modify.swift) @@ -131,7 +131,7 @@ Ensure successive `next()` calls mutate generator state (no repeated constant)
-`testReplacesAndTrimsStrings` +- [ ] `testReplacesAndTrimsStrings` Cover replacements by set/value and trimming behaviors including empty-line removal @@ -139,7 +139,7 @@ Cover replacements by set/value and trimming behaviors including empty-line remo
-`testTrimsTrailingCharacters` +- [ ] `testTrimsTrailingCharacters` Verify targeted trailing whitespace/newline removal paths @@ -147,7 +147,7 @@ Verify targeted trailing whitespace/newline removal paths
-`testRepeatsStringWithMultiplicationOperator` +- [ ] `testRepeatsStringWithMultiplicationOperator` Ensure `"abc" * 3` returns expected concatenation @@ -155,7 +155,7 @@ Ensure `"abc" * 3` returns expected concatenation
-`testStringOffsetPresetsEmitExpectedTokens` +- [ ] `testStringOffsetPresetsEmitExpectedTokens` Validate `StringOffset` preset suffix/prefix strings and computed properties @@ -163,7 +163,7 @@ Validate `StringOffset` preset suffix/prefix strings and computed properties
-`testOffsetsMultilineBlocks` +- [ ] `testOffsetsMultilineBlocks` Assert offsetting helpers pad each line as documented @@ -174,7 +174,7 @@ Assert offsetting helpers pad each line as documented
-## ⊞ [Structures/Dimensions/GeometryReceivers.swift](Structures/Dimensions/GeometryReceivers.swift) +## - [ ] ⊞ [Structures/Dimensions/GeometryReceivers.swift](Structures/Dimensions/GeometryReceivers.swift) @@ -194,7 +194,7 @@ Assert offsetting helpers pad each line as documented
-`testCapturesWidthAndHeightPreferences` +- [ ] `testCapturesWidthAndHeightPreferences` Inject test views and confirm bindings receive geometry values once layout occurs @@ -202,7 +202,7 @@ Inject test views and confirm bindings receive geometry values once layout occur
-`testInvokesCallbacksOnChange` +- [ ] `testInvokesCallbacksOnChange` Ensure callbacks fire with updated dimensions when layout changes @@ -210,7 +210,7 @@ Ensure callbacks fire with updated dimensions when layout changes
-`testBindsOptionalAndNonoptionalInsets` +- [ ] `testBindsOptionalAndNonoptionalInsets` Verify both `EdgeInsets` and `EdgeInsets?` bindings are updated through the preference chain @@ -221,7 +221,7 @@ Verify both `EdgeInsets` and `EdgeInsets?` bindings are updated through the pref
-## ⊞ [Structures/Dimensions/RelativeDimension.swift](Structures/Dimensions/RelativeDimension.swift) +## - [ ] ⊞ [Structures/Dimensions/RelativeDimension.swift](Structures/Dimensions/RelativeDimension.swift) @@ -243,7 +243,7 @@ Verify both `EdgeInsets` and `EdgeInsets?` bindings are updated through the pref
-`testInitializesFromLiterals` +- [ ] `testInitializesFromLiterals` Confirm float/integer literal initializers map to `.exactly` with converted `CGFloat` @@ -251,7 +251,7 @@ Confirm float/integer literal initializers map to `.exactly` with converted `CGF
-`testExposesValueAndMaxValue` +- [ ] `testExposesValueAndMaxValue` Validate optional outputs for `exactly` vs `maximum` cases @@ -259,7 +259,7 @@ Validate optional outputs for `exactly` vs `maximum` cases
-`testMinimumCaseReportsPadding` +- [ ] `testMinimumCaseReportsPadding` Ensure `.minimum` carries the provided padding @@ -267,7 +267,7 @@ Ensure `.minimum` carries the provided padding
-`testComparisonHelpersMatchCases` +- [ ] `testComparisonHelpersMatchCases` Test `is` and convenience flags across all permutations @@ -278,7 +278,7 @@ Test `is` and convenience flags across all permutations
-## ⊞ [Structures/Type Erase/AnyComparable.swift](Structures/Type%20Erase/AnyComparable.swift) +## - [ ] ⊞ [Structures/Type Erase/AnyComparable.swift](Structures/Type%20Erase/AnyComparable.swift) @@ -300,7 +300,7 @@ Test `is` and convenience flags across all permutations
-`testComparesBoxedValues` +- [ ] `testComparesBoxedValues` Assert `<` and `==` use underlying `Comparable` semantics for same-typed boxes @@ -308,7 +308,7 @@ Assert `<` and `==` use underlying `Comparable` semantics for same-typed boxes
-`testHandlesCrossTypeComparisonsSafely` +- [ ] `testHandlesCrossTypeComparisonsSafely` Ensure comparisons with different underlying types return `false` without crashes @@ -316,7 +316,7 @@ Ensure comparisons with different underlying types return `false` without crashe
-`testWrapsComparableValues` +- [ ] `testWrapsComparableValues` Verify `.asAnyComparable()` wraps and preserves ordering in sorted collections @@ -327,7 +327,7 @@ Verify `.asAnyComparable()` wraps and preserves ordering in sorted collections
-## ⊞ [Structures/Type Erase/AnyHashableAndSendable.swift](Structures/Type%20Erase/AnyHashableAndSendable.swift) +## - [ ] ⊞ [Structures/Type Erase/AnyHashableAndSendable.swift](Structures/Type%20Erase/AnyHashableAndSendable.swift) @@ -349,7 +349,7 @@ Verify `.asAnyComparable()` wraps and preserves ordering in sorted collections
-`testBoxesPreserveHashAndEquality` +- [ ] `testBoxesPreserveHashAndEquality` Verify wrappers round-trip `Hashable`/`Sendable` values and compare correctly across identical and differing types @@ -357,7 +357,7 @@ Verify wrappers round-trip `Hashable`/`Sendable` values and compare correctly ac
-`testEncodesWrappedValues` +- [ ] `testEncodesWrappedValues` Ensure encodable wrappers forward encoding to the underlying value and produce expected JSON/JSON5 strings @@ -365,7 +365,7 @@ Ensure encodable wrappers forward encoding to the underlying value and produce e
-`testSupportsPropertyWrapperInitStyles` +- [ ] `testSupportsPropertyWrapperInitStyles` Cover both `init(wrappedValue:)` and direct initializers for each wrapper @@ -373,7 +373,7 @@ Cover both `init(wrappedValue:)` and direct initializers for each wrapper
-`testHandlesNonEncodableDictionaryEntries` +- [ ] `testHandlesNonEncodableDictionaryEntries` Assert encoding helpers return the fallback message when dictionary cannot be cast to `Encodable` @@ -384,7 +384,7 @@ Assert encoding helpers return the fallback message when dictionary cannot be ca
-## ⊞ [Ugly/WindowUtils.swift](Ugly/WindowUtils.swift) +## - [ ] ⊞ [Ugly/WindowUtils.swift](Ugly/WindowUtils.swift) @@ -405,7 +405,7 @@ Assert encoding helpers return the fallback message when dictionary cannot be ca
-`testTracksManuallyAssignedWindow` +- [ ] `testTracksManuallyAssignedWindow` Confirm `setOriginalWindow` overrides lookup and restores when cleared @@ -413,7 +413,7 @@ Confirm `setOriginalWindow` overrides lookup and restores when cleared
-`testResolvesTopViewController` +- [ ] `testResolvesTopViewController` Simulate navigation/tab/presentation stacks to ensure the traversal selects the visible controller @@ -421,7 +421,7 @@ Simulate navigation/tab/presentation stacks to ensure the traversal selects the
-`testEndsEditingThroughCurrentWindow` +- [ ] `testEndsEditingThroughCurrentWindow` Verify `endEditing(force:)` relays to the active window and respects the `force` flag @@ -429,7 +429,7 @@ Verify `endEditing(force:)` relays to the active window and respects the `force`
-`testReportsSafeAreaInsets` +- [ ] `testReportsSafeAreaInsets` Validate `SafeFrame.currentInsets` mirrors the active window's safe area @@ -440,7 +440,7 @@ Validate `SafeFrame.currentInsets` mirrors the active window's safe area
-## ⊞ [Third Party/Pluralize/Pluralize.swift](Third%20Party/Pluralize/Pluralize.swift) +## - [x] ⊞ [Third Party/Pluralize/Pluralize.swift](Third%20Party/Pluralize/Pluralize.swift) @@ -460,7 +460,7 @@ Validate `SafeFrame.currentInsets` mirrors the active window's safe area
-`testPluralizesAndSingularizesCommonWords` +- [x] `testPluralizesAndSingularizesCommonWords` — Implemented in `Tests/QizhKitTests/PluralizeTests.swift`. Verifies regular and irregular word pluralization/singularization including common words, Latin/Greek endings, and the `count`/`with` parameters. Check irregular and regular transformations for representative samples @@ -468,7 +468,7 @@ Check irregular and regular transformations for representative samples
-`testHonorsUncountableAndUnchangingLists` +- [x] `testHonorsUncountableAndUnchangingLists` — Implemented in `Tests/QizhKitTests/PluralizeTests.swift`. Validates that uncountable words (information, equipment, etc.) and unchanging words (sheep, deer, etc.) remain unchanged when pluralized/singularized. Confirm words in those collections return unchanged results @@ -476,7 +476,7 @@ Confirm words in those collections return unchanged results
-`testAddsRuntimeRules` +- [x] `testAddsRuntimeRules` — Implemented in `Tests/QizhKitTests/PluralizeTests.swift`. Verifies that dynamically added plural/singular rules via `Pluralize.rule()`, `Pluralize.singularRule()`, `Pluralize.uncountable()`, and `Pluralize.unchanging()` are applied correctly. Ensure dynamically added plural/singular rules apply ahead of defaults diff --git a/Tests/QizhKitTests/PluralizeTests.swift b/Tests/QizhKitTests/PluralizeTests.swift new file mode 100644 index 0000000..d71587d --- /dev/null +++ b/Tests/QizhKitTests/PluralizeTests.swift @@ -0,0 +1,152 @@ +// +// PluralizeTests.swift +// QizhKit +// +// Created by GitHub Copilot on 06.12.2025. +// + +import Testing +@testable import QizhKit + +/// Tests for the Pluralize class and String pluralization extensions. +/// Validates plural/singular transformations, uncountable/unchanging word handling, +/// and dynamic rule additions. +@Suite("Pluralize tests") +struct PluralizeTests { + + // MARK: - testPluralizesAndSingularizesCommonWords + + /// Verifies that common words are correctly pluralized and singularized, + /// including both regular and irregular transformations. + @Test func testPluralizesAndSingularizesCommonWords() async throws { + // Regular pluralization rules + #expect("cat".pluralize() == "cats") + #expect("dog".pluralize() == "dogs") + #expect("house".pluralize() == "houses") + + // Words ending in 'y' preceded by consonant -> 'ies' + #expect("city".pluralize() == "cities") + #expect("baby".pluralize() == "babies") + #expect("party".pluralize() == "parties") + + // Words ending in 'x' -> 'xes' + #expect("box".pluralize() == "boxes") + #expect("tax".pluralize() == "taxes") + + // Words ending in 'sh', 'ss', 'zz' -> 'es' + #expect("wish".pluralize() == "wishes") + #expect("class".pluralize() == "classes") + #expect("buzz".pluralize() == "buzzes") + + // Words ending in 'ch' -> 'ches' + #expect("watch".pluralize() == "watches") + #expect("church".pluralize() == "churches") + + // Irregular plurals + #expect("tooth".pluralize() == "teeth") + #expect("foot".pluralize() == "feet") + #expect("goose".pluralize() == "geese") + #expect("mouse".pluralize() == "mice") + #expect("child".pluralize() == "children") + #expect("person".pluralize() == "people") + #expect("man".pluralize() == "men") + #expect("woman".pluralize() == "women") + #expect("ox".pluralize() == "oxen") + + // Words ending in 'f' or 'fe' -> 'ves' + #expect("knife".pluralize() == "knives") + #expect("wolf".pluralize() == "wolves") + #expect("leaf".pluralize() == "leaves") + #expect("thief".pluralize() == "thieves") + + // Latin/Greek endings + #expect("cactus".pluralize() == "cacti") + #expect("nucleus".pluralize() == "nuclei") + #expect("fungus".pluralize() == "fungi") + #expect("criterion".pluralize() == "criteria") + #expect("phenomenon".pluralize() == "phenomena") + + // Singularization tests + #expect("cats".singularize() == "cat") + #expect("dogs".singularize() == "dog") + #expect("cities".singularize() == "city") + #expect("boxes".singularize() == "box") + #expect("wishes".singularize() == "wish") + #expect("watches".singularize() == "watch") + #expect("analyses".singularize() == "analysis") + #expect("diagnoses".singularize() == "diagnosis") + + // Count parameter tests + #expect("cat".pluralize(count: 1) == "cat") + #expect("cat".pluralize(count: 2) == "cats") + #expect("cat".pluralize(count: 0) == "cats") + + // Custom plural with 'with' parameter + #expect("person".pluralize(count: 2, with: "persons") == "persons") + } + + // MARK: - testHonorsUncountableAndUnchangingLists + + /// Verifies that uncountable and unchanging words remain unchanged + /// when pluralized or singularized. + @Test func testHonorsUncountableAndUnchangingLists() async throws { + // Uncountable words should not change + #expect("information".pluralize() == "information") + #expect("equipment".pluralize() == "equipment") + #expect("news".pluralize() == "news") + #expect("money".pluralize() == "money") + #expect("rice".pluralize() == "rice") + #expect("music".pluralize() == "music") + #expect("furniture".pluralize() == "furniture") + #expect("luggage".pluralize() == "luggage") + #expect("advice".pluralize() == "advice") + #expect("traffic".pluralize() == "traffic") + #expect("software".pluralize() == "software") + #expect("research".pluralize() == "research") + #expect("knowledge".pluralize() == "knowledge") + #expect("education".pluralize() == "education") + + // Unchanging words (same in singular and plural) + #expect("sheep".pluralize() == "sheep") + #expect("deer".pluralize() == "deer") + #expect("moose".pluralize() == "moose") + #expect("swine".pluralize() == "swine") + #expect("bison".pluralize() == "bison") + #expect("corps".pluralize() == "corps") + #expect("means".pluralize() == "means") + #expect("series".pluralize() == "series") + #expect("scissors".pluralize() == "scissors") + #expect("species".pluralize() == "species") + + // Uncountable singularization should also return unchanged + #expect("information".singularize() == "information") + #expect("sheep".singularize() == "sheep") + + // Empty string should return unchanged + #expect("".pluralize() == "") + #expect("".singularize() == "") + } + + // MARK: - testAddsRuntimeRules + + /// Verifies that dynamically added plural and singular rules + /// are applied ahead of default rules. + @Test func testAddsRuntimeRules() async throws { + // Add a custom plural rule for a made-up pattern + // Note: Rules are added to the front of the list, so they take precedence + Pluralize.rule(rule: "zoon$", with: "$1zoa") + #expect("protozoon".pluralize() == "protozoa") + + // Add a custom singular rule + Pluralize.singularRule(rule: "zoa$", with: "zoon") + #expect("protozoa".singularize() == "protozoon") + + // Add a custom uncountable word + Pluralize.uncountable(word: "customword") + #expect("customword".pluralize() == "customword") + + // Add a custom unchanging word + Pluralize.unchanging(word: "unchangingtest") + #expect("unchangingtest".pluralize() == "unchangingtest") + } +} From e9cc624d7b5555d4b018d9543df904a6a2fc0e64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 01:13:38 +0000 Subject: [PATCH 3/3] Fix regex patterns in testAddsRuntimeRules to use proper capturing groups Co-authored-by: qizh <941415+qizh@users.noreply.github.com> --- Tests/QizhKitTests/PluralizeTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/QizhKitTests/PluralizeTests.swift b/Tests/QizhKitTests/PluralizeTests.swift index d71587d..540610f 100644 --- a/Tests/QizhKitTests/PluralizeTests.swift +++ b/Tests/QizhKitTests/PluralizeTests.swift @@ -134,11 +134,12 @@ struct PluralizeTests { @Test func testAddsRuntimeRules() async throws { // Add a custom plural rule for a made-up pattern // Note: Rules are added to the front of the list, so they take precedence - Pluralize.rule(rule: "zoon$", with: "$1zoa") + // The pattern uses a capturing group for the prefix + Pluralize.rule(rule: "(proto)zoon$", with: "$1zoa") #expect("protozoon".pluralize() == "protozoa") // Add a custom singular rule - Pluralize.singularRule(rule: "zoa$", with: "zoon") + Pluralize.singularRule(rule: "(proto)zoa$", with: "$1zoon") #expect("protozoa".singularize() == "protozoon") // Add a custom uncountable word