Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New SendableProperty property wrapper #125

Merged
merged 3 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public final class SendableArray<T>: @unchecked Sendable, Sequence {
/// Internal collection
private(set) var content: [T]

public init(content: [T] = Array<T>()) {
public init(content: [T] = [T]()) {
self.content = content
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

/// Making a property thread safe, while not requiring `await`. Conforms to Sendable.
///
/// Please prefer using first party structured concurrency. Use this for prototyping or dealing with race conditions.
@propertyWrapper
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class SendableProperty<Property>: @unchecked Sendable {
/// Serial locking queue
let lock = DispatchQueue(label: "com.infomaniak.core.SendableProperty.lock")

/// Store property
var property: Property?

public init() { }

public var wrappedValue: Property? {
get {
lock.sync {
return self.property
}
}
set {
lock.sync {
self.property = newValue
}
}
}

/// The property wrapper itself for debugging and testing
public var projectedValue: SendableProperty {
self
}
}
176 changes: 176 additions & 0 deletions Tests/InfomaniakCoreTests/Sendable/UTSendableArray.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import InfomaniakCore
import XCTest

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final class UTSendableArray: XCTestCase {
func testInsertSubscript() async {
// GIVEN
let collection = SendableArray<String>()

// WHEN
let t = Task.detached {
collection[0] = "a"
collection[1] = "b"
collection[2] = "c"
}

_ = await t.result

// THEN
XCTAssertEqual(collection.count, 3)
}

func testInsert() async {
// GIVEN
let collection = SendableArray<String>()

// WHEN
let t = Task.detached {
collection.append("a")
collection.append("b")
}

_ = await t.result

// THEN
XCTAssertEqual(collection.count, 2)
}

func testUpdate() async {
// GIVEN
let collection = SendableArray<String>()

// WHEN
let t = Task.detached {
collection.append("a")
collection[0] = "b"
collection[1] = "c"
}

_ = await t.result

// THEN
XCTAssertEqual(collection.count, 2)
}

func testRemoveAll() async {
// GIVEN
let collection = SendableArray<String>()
collection.append("a")
collection.append("b")

// WHEN
let t = Task.detached {
collection.removeAll()
}

_ = await t.result

// THEN
XCTAssertTrue(collection.isEmpty)
}

func testRemoveAllWhere() async {
// GIVEN
let collection = SendableArray<String>()
collection.append("a")
collection.append("b")
collection.append("c")

// WHEN
let t = Task.detached {
collection.removeAll(where: { $0 == "b" })
}

_ = await t.result

// THEN
XCTAssertFalse(collection.values.contains("b"))
}

func testIterator() async {
// GIVEN
let collection = SendableArray<String>()
collection.append("a")
collection.append("b")

var iterator = collection.makeIterator()

// WHEN
// We remove all items in the collection
let t = Task.detached {
collection.removeAll()
}

await t.finish()

// THEN
XCTAssertTrue(collection.values.isEmpty, "The collection is expected to be empty")

// We can work with the captured enumeration
var isEmpty = true
while let value = iterator.next() {
isEmpty = false

if value == "a" || value == "b" {
// OK
} else {
XCTFail("unexpected value:\(value)")
}
}

XCTAssertFalse(isEmpty, "the iterator is not supposed to be empty")
}

func testEnumerated() async {
// GIVEN
let collection = SendableArray<String>()
collection.append("a")
collection.append("b")

let collectionEnumerated = collection.enumerated()

// WHEN
// We remove all items in the collection
let t = Task.detached {
collection.removeAll()
}

await t.finish()

// THEN
XCTAssertTrue(collection.values.isEmpty, "The collection is expected to be empty")

// We can work with the captured enumeration
var isEmpty = true
for (index, value) in collectionEnumerated {
isEmpty = false

if value == "a" || value == "b" {
// OK
} else {
XCTFail("unexpected value:\(value) at index:\(index)")
}
}

XCTAssertFalse(isEmpty, "the iterator is not supposed to be empty")
}
}
147 changes: 147 additions & 0 deletions Tests/InfomaniakCoreTests/Sendable/UTSendableDictionary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import InfomaniakCore
import XCTest

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final class UTSendableDictionary: XCTestCase {
func testInsertSubscript() async {
// GIVEN
let collection = SendableDictionary<String, Int>()

// WHEN
let t = Task.detached {
collection["a"] = 1
collection["b"] = 2
}

_ = await t.result

// THEN
XCTAssertEqual(collection.count, 2)
}

func testInsert() async {
// GIVEN
let collection = SendableDictionary<String, Int>()

// WHEN
let t = Task.detached {
collection.setValue(1, for: "a")
collection.setValue(2, for: "b")
}

_ = await t.result

// THEN
XCTAssertEqual(collection.count, 2)
}

func testRemoveAll() async {
// GIVEN
let collection = SendableDictionary<String, Int>()
collection.setValue(1, for: "a")
collection.setValue(2, for: "b")

// WHEN
let t = Task.detached {
collection.removeAll()
}

_ = await t.result

// THEN
XCTAssertTrue(collection.values.isEmpty)
}

func testIterator() async {
// GIVEN
let collection = SendableDictionary<String, Int>()
collection.setValue(1, for: "a")
collection.setValue(2, for: "b")

var iterator = collection.makeIterator()

// WHEN
// We remove all items in the collection
let t = Task.detached {
collection.removeAll()
}

await t.finish()

// THEN
XCTAssertTrue(collection.values.isEmpty, "The collection is expected to be empty")

// We can work with the captured enumeration
var isEmpty = true
while let (key, value) = iterator.next() {
isEmpty = false

if key == "a" {
XCTAssertEqual(value, 1)
} else if key == "b" {
XCTAssertEqual(value, 2)
} else {
XCTFail("unexpected key:\(key) value:\(value)")
}
}

XCTAssertFalse(isEmpty, "the iterator is not supposed to be empty")
}

func testEnumerated() async {
// GIVEN
let collection = SendableDictionary<String, Int>()
collection.setValue(1, for: "a")
collection.setValue(2, for: "b")

let collectionEnumerated = collection.enumerated()

// WHEN
// We remove all items in the collection
let t = Task.detached {
collection.removeAll()
}

await t.finish()

// THEN
XCTAssertTrue(collection.values.isEmpty, "The collection is expected to be empty")

// We can work with the captured enumeration
var isEmpty = true
for (index, node) in collectionEnumerated {
isEmpty = false

let key = node.0
let value = node.1

if key == "a" {
XCTAssertEqual(value, 1)
} else if key == "b" {
XCTAssertEqual(value, 2)
} else {
XCTFail("unexpected key:\(key) value:\(value) at index:\(index) ")
}
}

XCTAssertFalse(isEmpty, "the iterator is not supposed to be empty")
}
}
Loading
Loading