From 2bba6142ca98b076ab5bf4da90104161e0b2ace6 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sun, 21 Dec 2025 15:09:34 +0400 Subject: [PATCH 1/2] tests: implement unit tests --- Tests/AtomicTests/AtomicTests.swift | 309 +++++++++++++++++++++++++++- mise/tasks/install.sh | 57 +++++ 2 files changed, 355 insertions(+), 11 deletions(-) diff --git a/Tests/AtomicTests/AtomicTests.swift b/Tests/AtomicTests/AtomicTests.swift index 9a7c2a6..75c41a3 100644 --- a/Tests/AtomicTests/AtomicTests.swift +++ b/Tests/AtomicTests/AtomicTests.swift @@ -12,20 +12,34 @@ final class AtomicTests: XCTestCase, @unchecked Sendable { // MARK: Properties @Atomic private var dict = [Int: String]() + @Atomic private var counter = 0 + @Atomic private var test = 4 + private let value = Atomic(wrappedValue: 5) - @Atomic - private var test = 4 + // MARK: Setup + + override func setUp() { + super.setUp() + _dict.write([:]) + _counter.write(0) + _test.write(4) + } - // MARK: Tests + // MARK: Tests - Basic Operations - func test_thatAtomicPropertyChangesValue() { + func test_thatDictionaryChangesValue_whenConcurrentWritesOccur() { DispatchQueue.concurrentPerform(iterations: .iterations) { _ in - self.dict[.random(in: 0 ... 1000)] = "test" + _dict.write { dict in + dict[.random(in: 0 ... 1000)] = "test" + } } + + let finalCount = _dict.read { $0.count } + XCTAssertGreaterThan(finalCount, 0) } - func test_thatAtomicPropertyReadValue() { + func test_thatValueChanges_whenConcurrentReadsAndWritesOccur() { // given let initialValue = value.read { $0 } @@ -39,21 +53,222 @@ final class AtomicTests: XCTestCase, @unchecked Sendable { XCTAssertNotEqual(value.read { $0 }, initialValue) } - func test_thatAtomicPropertyAreSetSafely() { + func test_thatValueIsSet_whenConcurrentWritesOccur() { // given struct Mutable { var value = 1 } let mutable = Atomic(wrappedValue: .init()) // when DispatchQueue.concurrentPerform(iterations: .iterations) { i in - mutable.value = i + mutable.write(Mutable(value: i)) + } + + // then + XCTAssertNotEqual(mutable.read { $0.value }, 1) + } + + // MARK: Tests - Read/Write Methods + + func test_thatReadReturnsCurrentValue_whenValueIsSet() { + // given + _counter.write(42) + + // when + let result = _counter.read { $0 } + + // then + XCTAssertEqual(result, 42) + } + + func test_thatValueIsModified_whenWriteClosureExecutes() { + // given + _counter.write(0) + + // when + _counter.write { value in + value += 10 + } + + // then + let result = _counter.read { $0 } + XCTAssertEqual(result, 10) + } + + func test_thatValueIsSet_whenWriteValueCalled() { + // given + _counter.write(0) + + // when + _counter.write(100) + + // then + let result = _counter.read { $0 } + XCTAssertEqual(result, 100) + } + + func test_thatClosureResultReturned_whenWriteExecutes() { + // given + _counter.write(5) + + // when + let result = _counter.write { value -> String in + value *= 2 + return "Result: \(value)" + } + + // then + XCTAssertEqual(result, "Result: 10") + XCTAssertEqual(_counter.read { $0 }, 10) + } + + // MARK: Tests - Thread Safety + + func test_thatAllReadsReturnSameValue_whenConcurrentReadsOccur() { + // given + _counter.write(100) + var results: [Int] = [] + let resultsQueue = DispatchQueue(label: "results.queue") + + // when + DispatchQueue.concurrentPerform(iterations: .iterations) { _ in + let value = _counter.read { $0 } + resultsQueue.sync { + results.append(value) + } + } + + // then + XCTAssertEqual(results.count, .iterations) + XCTAssertTrue(results.allSatisfy { $0 == 100 }) + } + + func test_thatFinalValueIsCorrect_whenConcurrentWritesOccur() { + // given + _counter.write(0) + + // when + DispatchQueue.concurrentPerform(iterations: .iterations) { _ in + _counter.write { $0 += 1 } + } + + // then + let finalValue = _counter.read { $0 } + XCTAssertEqual(finalValue, .iterations) + } + + func test_thatOperationsAreSafe_whenMixedReadWritesOccur() { + // given + _counter.write(0) + var readValues: [Int] = [] + let readQueue = DispatchQueue(label: "read.queue") + + // when + DispatchQueue.concurrentPerform(iterations: .iterations) { i in + if i % 2 == 0 { + _counter.write { $0 += 1 } + } else { + let value = _counter.read { $0 } + readQueue.sync { + readValues.append(value) + } + } + } + + // then + let finalValue = _counter.read { $0 } + XCTAssertEqual(finalValue, .iterations / 2) + XCTAssertEqual(readValues.count, .iterations / 2) + } + + // MARK: Tests - Dynamic Member Lookup + + func test_thatPropertyValuesReturned_whenDynamicMemberLookupGets() { + // given + struct Config { + var timeout: Double = 30.0 + var retryCount: Int = 3 + } + let config = Atomic(wrappedValue: Config()) + + // when + let timeout = config.timeout + let retryCount = config.retryCount + + // then + XCTAssertEqual(timeout, 30.0) + XCTAssertEqual(retryCount, 3) + } + + func test_thatPropertyValuesSet_whenDynamicMemberLookupSets() { + // given + struct Config { + var timeout: Double = 30.0 + var retryCount: Int = 3 + } + let config = Atomic(wrappedValue: Config()) + + // when + config.timeout = 60.0 + config.retryCount = 5 + + // then + XCTAssertEqual(config.timeout, 60.0) + XCTAssertEqual(config.retryCount, 5) + } + + func test_thatOperationsAreSafe_whenDynamicMemberLookupUsedConcurrently() { + // given + struct Counter { + var value: Int = 0 + } + let atomicCounter = Atomic(wrappedValue: Counter()) + + // when + DispatchQueue.concurrentPerform(iterations: .iterations) { _ in + atomicCounter.write { $0.value += 1 } + } + + // then + XCTAssertEqual(atomicCounter.value, .iterations) + } + + // MARK: Tests - Collections + + func test_thatAllValuesStored_whenDictionaryWrittenConcurrently() { + // given + _dict.write([:]) + + // when + DispatchQueue.concurrentPerform(iterations: .iterations) { i in + _dict.write { dict in + dict[i] = "value_\(i)" + } } // then - XCTAssertNotEqual(mutable.value, 1) + let finalCount = _dict.read { $0.count } + XCTAssertEqual(finalCount, .iterations) } - func test_thatAtomicPropertyEqual() { + func test_thatAllItemsAppended_whenArrayWrittenConcurrently() { + // given + @Atomic var array: [Int] = [] + + // when + DispatchQueue.concurrentPerform(iterations: .iterations) { i in + _array.write { arr in + arr.append(i) + } + } + + // then + let finalCount = _array.read { $0.count } + XCTAssertEqual(finalCount, .iterations) + } + + // MARK: Tests - Equatable & Hashable + + func test_thatValuesAreEqual_whenWrappedValuesMatch() { // given let value1 = Atomic(wrappedValue: 1) let value2 = Atomic(wrappedValue: 1) @@ -62,7 +277,16 @@ final class AtomicTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(value1, value2) } - func test_thatAtomicPropertyConfrmancesToHashable() { + func test_thatValuesAreNotEqual_whenWrappedValuesDiffer() { + // given + let value1 = Atomic(wrappedValue: 1) + let value2 = Atomic(wrappedValue: 2) + + // then + XCTAssertNotEqual(value1, value2) + } + + func test_thatHashValuesMatch_whenWrappedValuesAreEqual() { // given struct Mutable: Hashable { var value = 1 } let mutable1 = Atomic(wrappedValue: .init()) @@ -71,6 +295,69 @@ final class AtomicTests: XCTestCase, @unchecked Sendable { // then XCTAssertEqual(mutable1.hashValue, mutable2.hashValue) } + + // MARK: Tests - Complex Scenarios + + func test_thatNestedModificationsWork_whenWriteClosureExecutes() { + // given + struct Data { + var items: [String] = [] + var count: Int = 0 + } + let data = Atomic(wrappedValue: Data()) + + // when + data.write { value in + value.items.append("item1") + value.count = value.items.count + + value.items.append("item2") + value.count = value.items.count + } + + // then + let result = data.read { ($0.items.count, $0.count) } + XCTAssertEqual(result.0, 2) + XCTAssertEqual(result.1, 2) + } + + func test_thatValueRemainsUnchanged_whenThrowingClosureThrows() { + // given + enum TestError: Error { + case testError + } + _counter.write(10) + + // when/then + XCTAssertThrowsError(try _counter.write { value -> Int in + if value > 5 { + throw TestError.testError + } + return value + }) + + // Verify value wasn't modified + XCTAssertEqual(_counter.read { $0 }, 10) + } + + func test_thatTransformedValueReturned_whenReadThrowingClosureSucceeds() throws { + // given + enum TestError: Error { + case testError + } + _counter.write(10) + + // when + let result = try _counter.read { value -> Int in + guard value > 0 else { + throw TestError.testError + } + return value * 2 + } + + // then + XCTAssertEqual(result, 20) + } } // MARK: Constants diff --git a/mise/tasks/install.sh b/mise/tasks/install.sh index e69de29..aeba03b 100755 --- a/mise/tasks/install.sh +++ b/mise/tasks/install.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +set -e + +echo "🔧 Installing git hooks..." + +# Find git repository root +GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) + +if [ -z "$GIT_ROOT" ]; then + echo "❌ Error: Not a git repository" + exit 1 +fi + +echo "📁 Git root: $GIT_ROOT" + +# Create hooks directory if it doesn't exist +mkdir -p "$GIT_ROOT/.git/hooks" + +# Create pre-commit hook +cat > "$GIT_ROOT/.git/hooks/pre-commit" <<'HOOK_EOF' +#!/bin/bash + +echo "🔍 Running linters..." + +echo "📝 Formatting staged Swift files..." +git diff --diff-filter=d --staged --name-only | grep -e '\.swift$' | while read line; do + if [[ $line == *"/Generated"* ]]; then + echo "⏭️ Skipping generated file: $line" + else + echo "✨ Formatting: $line" + mise exec swiftformat -- swiftformat "${line}" + git add "$line" + fi +done + +if ! mise run lint; then + echo "❌ Lint failed. Please fix the issues before committing." + echo "💡 Tip: Run 'mise run format' to auto-fix some issues" + echo "⚠️ To skip this hook, use: git commit --no-verify" + exit 1 +fi + +echo "✅ All checks passed!" +exit 0 +HOOK_EOF + +chmod +x "$GIT_ROOT/.git/hooks/pre-commit" + +echo "✅ Git hooks installed successfully!" +echo "📍 Hook location: $GIT_ROOT/.git/hooks/pre-commit" +echo "" +echo "Pre-commit hook will:" +echo " 1. Format staged Swift files (except /Generated)" +echo " 2. Run mise lint" +echo "" +echo "To skip the hook, use: git commit --no-verify" \ No newline at end of file From 5a5171dca9b8798789101bc6100a8dacfe1f1cbc Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sun, 21 Dec 2025 15:10:02 +0400 Subject: [PATCH 2/2] chore: bump the swift version --- .swiftformat | 2 +- Tests/AtomicTests/AtomicTests.swift | 46 ++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.swiftformat b/.swiftformat index c57f2b6..e459525 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,6 +1,6 @@ # Stream rules ---swiftversion 5.3 +--swiftversion 5.10 # Use 'swiftformat --options' to list all of the possible options diff --git a/Tests/AtomicTests/AtomicTests.swift b/Tests/AtomicTests/AtomicTests.swift index 75c41a3..3b71a39 100644 --- a/Tests/AtomicTests/AtomicTests.swift +++ b/Tests/AtomicTests/AtomicTests.swift @@ -126,20 +126,17 @@ final class AtomicTests: XCTestCase, @unchecked Sendable { func test_thatAllReadsReturnSameValue_whenConcurrentReadsOccur() { // given _counter.write(100) - var results: [Int] = [] - let resultsQueue = DispatchQueue(label: "results.queue") + let results = Box<[Int]>([]) // when DispatchQueue.concurrentPerform(iterations: .iterations) { _ in let value = _counter.read { $0 } - resultsQueue.sync { - results.append(value) - } + results.mutate { $0.append(value) } } // then - XCTAssertEqual(results.count, .iterations) - XCTAssertTrue(results.allSatisfy { $0 == 100 }) + XCTAssertEqual(results.value.count, .iterations) + XCTAssertTrue(results.value.allSatisfy { $0 == 100 }) } func test_thatFinalValueIsCorrect_whenConcurrentWritesOccur() { @@ -159,8 +156,7 @@ final class AtomicTests: XCTestCase, @unchecked Sendable { func test_thatOperationsAreSafe_whenMixedReadWritesOccur() { // given _counter.write(0) - var readValues: [Int] = [] - let readQueue = DispatchQueue(label: "read.queue") + let readValues = Box<[Int]>([]) // when DispatchQueue.concurrentPerform(iterations: .iterations) { i in @@ -168,16 +164,14 @@ final class AtomicTests: XCTestCase, @unchecked Sendable { _counter.write { $0 += 1 } } else { let value = _counter.read { $0 } - readQueue.sync { - readValues.append(value) - } + readValues.mutate { $0.append(value) } } } // then let finalValue = _counter.read { $0 } XCTAssertEqual(finalValue, .iterations / 2) - XCTAssertEqual(readValues.count, .iterations / 2) + XCTAssertEqual(readValues.value.count, .iterations / 2) } // MARK: Tests - Dynamic Member Lookup @@ -253,10 +247,11 @@ final class AtomicTests: XCTestCase, @unchecked Sendable { func test_thatAllItemsAppended_whenArrayWrittenConcurrently() { // given @Atomic var array: [Int] = [] + let atomicArray = _array // when DispatchQueue.concurrentPerform(iterations: .iterations) { i in - _array.write { arr in + atomicArray.write { arr in arr.append(i) } } @@ -360,6 +355,29 @@ final class AtomicTests: XCTestCase, @unchecked Sendable { } } +// MARK: AtomicTests.Box + +extension AtomicTests { + private class Box: @unchecked Sendable { + private let lock = NSLock() + private var _value: T + + init(_ value: T) { + _value = value + } + + var value: T { + lock.lock(); defer { lock.unlock() } + return _value + } + + func mutate(_ block: (inout T) -> Void) { + lock.lock(); defer { lock.unlock() } + block(&_value) + } + } +} + // MARK: Constants private extension Int {