Skip to content

Commit 1363e8a

Browse files
authored
test to demonstrate item's state management (#32)
motivation: add tests to demonstrate how to deal with stateful "items" changes: * improve & document the "external state" test. * add test to demonstrate how state can be managed internally
1 parent 0052995 commit 1363e8a

File tree

2 files changed

+94
-11
lines changed

2 files changed

+94
-11
lines changed

Tests/ServiceLauncherTests/LifecycleTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ extension Tests {
5151
("testRegisterNIOClosure", testRegisterNIOClosure),
5252
("testRegisterShutdownNIOClosure", testRegisterShutdownNIOClosure),
5353
("testNIOFailure", testNIOFailure),
54+
("testInternalState", testInternalState),
5455
("testExternalState", testExternalState),
5556
]
5657
}

Tests/ServiceLauncherTests/LifecycleTests.swift

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,76 @@ final class Tests: XCTestCase {
705705
lifecycle.wait()
706706
}
707707

708+
// this is an example of how state can be managed inside a `LifecycleItem`
709+
// note the use of locks in this example since there could be concurrent access issues
710+
// in case shutdown is called (e.g. via signal trap) during the startup sequence
711+
// see also `testExternalState` test case
712+
func testInternalState() {
713+
class Item {
714+
enum State: Equatable {
715+
case idle
716+
case starting
717+
case started(String)
718+
case shuttingDown
719+
case shutdown
720+
}
721+
722+
var state = State.idle
723+
let stateLock = Lock()
724+
725+
let queue = DispatchQueue(label: "test")
726+
727+
let data: String
728+
729+
init(_ data: String) {
730+
self.data = data
731+
}
732+
733+
func start(callback: @escaping (Error?) -> Void) {
734+
self.stateLock.withLock {
735+
self.state = .starting
736+
}
737+
self.queue.asyncAfter(deadline: .now() + Double.random(in: 0.01 ... 0.1)) {
738+
self.stateLock.withLock {
739+
self.state = .started(self.data)
740+
}
741+
callback(nil)
742+
}
743+
}
744+
745+
func shutdown(callback: @escaping (Error?) -> Void) {
746+
self.stateLock.withLock {
747+
self.state = .shuttingDown
748+
}
749+
self.queue.asyncAfter(deadline: .now() + Double.random(in: 0.01 ... 0.1)) {
750+
self.stateLock.withLock {
751+
self.state = .shutdown
752+
}
753+
callback(nil)
754+
}
755+
}
756+
}
757+
758+
let expectedData = UUID().uuidString
759+
let item = Item(expectedData)
760+
let lifecycle = Lifecycle()
761+
lifecycle.register(label: "test",
762+
start: .async(item.start),
763+
shutdown: .async(item.shutdown))
764+
765+
lifecycle.start(configuration: .init(shutdownSignal: nil)) { error in
766+
XCTAssertNil(error, "not expecting error")
767+
XCTAssertEqual(item.state, .started(expectedData), "expected item to be shutdown, but \(item.state)")
768+
lifecycle.shutdown()
769+
}
770+
lifecycle.wait()
771+
XCTAssertEqual(item.state, .shutdown, "expected item to be shutdown, but \(item.state)")
772+
}
773+
774+
// this is an example of how state can be managed outside the `Lifecycle`
775+
// note the use of locks in this example since there could be concurrent access issues
776+
// in case shutdown is called (e.g. via signal trap) during the startup sequence
777+
// see also `testInternalState` test case, which is the prefered way to manage item's state
708778
func testExternalState() {
709779
enum State: Equatable {
710780
case idle
@@ -713,36 +783,48 @@ final class Tests: XCTestCase {
713783
}
714784

715785
class Item {
716-
let eventLoopGroup: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
786+
let queue = DispatchQueue(label: "test")
717787

718788
let data: String
789+
719790
init(_ data: String) {
720791
self.data = data
721792
}
722793

723-
func start() -> EventLoopFuture<String> {
724-
return self.eventLoopGroup.next().makeSucceededFuture(self.data)
794+
func start(callback: @escaping (String) -> Void) {
795+
self.queue.asyncAfter(deadline: .now() + Double.random(in: 0.01 ... 0.1)) {
796+
callback(self.data)
797+
}
725798
}
726799

727-
func shutdown() -> EventLoopFuture<Void> {
728-
return self.eventLoopGroup.next().makeSucceededFuture(())
800+
func shutdown(callback: @escaping () -> Void) {
801+
self.queue.asyncAfter(deadline: .now() + Double.random(in: 0.01 ... 0.1)) {
802+
callback()
803+
}
729804
}
730805
}
731806

732807
var state = State.idle
808+
let stateLock = Lock()
733809

734810
let expectedData = UUID().uuidString
735811
let item = Item(expectedData)
736812
let lifecycle = Lifecycle()
737813
lifecycle.register(label: "test",
738-
start: .eventLoopFuture {
739-
item.start().map { data -> Void in
740-
state = .started(data)
814+
start: .async { callback in
815+
item.start { data in
816+
stateLock.withLock {
817+
state = .started(data)
818+
}
819+
callback(nil)
741820
}
742821
},
743-
shutdown: .eventLoopFuture {
744-
item.shutdown().map { _ -> Void in
745-
state = .shutdown
822+
shutdown: .async { callback in
823+
item.shutdown {
824+
stateLock.withLock {
825+
state = .shutdown
826+
}
827+
callback(nil)
746828
}
747829
})
748830

0 commit comments

Comments
 (0)