From 3181361658ccd4eb7d798a632e83774748719af0 Mon Sep 17 00:00:00 2001 From: markspanbroek Date: Mon, 5 Jun 2023 10:48:06 +0200 Subject: [PATCH] Cleanup purchasing state machine (#422) * [state machine] Allow querying of state properties * [purchasing] use new state machine * [state machine] remove old state machine implementation * [purchasing] remove duplication in error handling --- codex/purchasing/purchase.nim | 9 +- codex/purchasing/statemachine.nim | 11 +- codex/purchasing/states/cancelled.nim | 24 ++-- codex/purchasing/states/error.nim | 12 +- codex/purchasing/states/errorhandling.nim | 9 ++ codex/purchasing/states/failed.nim | 10 +- codex/purchasing/states/finished.nim | 12 +- codex/purchasing/states/pending.nim | 24 ++-- codex/purchasing/states/started.nim | 33 +++-- codex/purchasing/states/submitted.nim | 23 ++-- codex/purchasing/states/unknown.nim | 49 ++++---- codex/rest/json.nim | 2 +- codex/utils/asyncstatemachine.nim | 8 ++ codex/utils/statemachine.nim | 130 -------------------- tests/codex/testpurchasing.nim | 98 +++++++-------- tests/codex/testutils.nim | 2 - tests/codex/utils/testasyncstatemachine.nim | 18 +++ tests/codex/utils/teststatemachine.nim | 48 -------- tests/codex/utils/teststatemachineasync.nim | 30 ----- 19 files changed, 173 insertions(+), 379 deletions(-) create mode 100644 codex/purchasing/states/errorhandling.nim delete mode 100644 codex/utils/statemachine.nim delete mode 100644 tests/codex/utils/teststatemachine.nim delete mode 100644 tests/codex/utils/teststatemachineasync.nim diff --git a/codex/purchasing/purchase.nim b/codex/purchasing/purchase.nim index 2301beff..451ab37c 100644 --- a/codex/purchasing/purchase.nim +++ b/codex/purchasing/purchase.nim @@ -44,10 +44,10 @@ func new*(_: type Purchase, return purchase proc start*(purchase: Purchase) = - purchase.switch(PurchasePending()) + purchase.start(PurchasePending()) proc load*(purchase: Purchase) = - purchase.switch(PurchaseUnknown()) + purchase.start(PurchaseUnknown()) proc wait*(purchase: Purchase) {.async.} = await purchase.future @@ -63,3 +63,8 @@ func error*(purchase: Purchase): ?(ref CatchableError) = some purchase.future.error else: none (ref CatchableError) + +func state*(purchase: Purchase): ?string = + proc description(state: State): string = + $state + purchase.query(description) diff --git a/codex/purchasing/statemachine.nim b/codex/purchasing/statemachine.nim index aab01026..de2753c3 100644 --- a/codex/purchasing/statemachine.nim +++ b/codex/purchasing/statemachine.nim @@ -1,21 +1,18 @@ -import ../utils/statemachine +import ../utils/asyncstatemachine import ../market import ../clock import ../errors export market export clock -export statemachine +export asyncstatemachine type - Purchase* = ref object of StateMachine + Purchase* = ref object of Machine future*: Future[void] market*: Market clock*: Clock requestId*: RequestId request*: ?StorageRequest - PurchaseState* = ref object of AsyncState + PurchaseState* = ref object of State PurchaseError* = object of CodexError - -method description*(state: PurchaseState): string {.base.} = - raiseAssert "description not implemented for state" diff --git a/codex/purchasing/states/cancelled.nim b/codex/purchasing/states/cancelled.nim index 93798adb..a0d8315b 100644 --- a/codex/purchasing/states/cancelled.nim +++ b/codex/purchasing/states/cancelled.nim @@ -1,20 +1,14 @@ import ../statemachine +import ./errorhandling import ./error -type PurchaseCancelled* = ref object of PurchaseState +type PurchaseCancelled* = ref object of ErrorHandlingState -method enterAsync*(state: PurchaseCancelled) {.async.} = - without purchase =? (state.context as Purchase): - raiseAssert "invalid state" - - try: - await purchase.market.withdrawFunds(purchase.requestId) - except CatchableError as error: - state.switch(PurchaseErrored(error: error)) - return - - let error = newException(Timeout, "Purchase cancelled due to timeout") - state.switch(PurchaseErrored(error: error)) - -method description*(state: PurchaseCancelled): string = +method `$`*(state: PurchaseCancelled): string = "cancelled" + +method run*(state: PurchaseCancelled, machine: Machine): Future[?State] {.async.} = + let purchase = Purchase(machine) + await purchase.market.withdrawFunds(purchase.requestId) + let error = newException(Timeout, "Purchase cancelled due to timeout") + return some State(PurchaseErrored(error: error)) diff --git a/codex/purchasing/states/error.nim b/codex/purchasing/states/error.nim index edddc192..df1c8d5c 100644 --- a/codex/purchasing/states/error.nim +++ b/codex/purchasing/states/error.nim @@ -3,11 +3,9 @@ import ../statemachine type PurchaseErrored* = ref object of PurchaseState error*: ref CatchableError -method enter*(state: PurchaseErrored) = - without purchase =? (state.context as Purchase): - raiseAssert "invalid state" - - purchase.future.fail(state.error) - -method description*(state: PurchaseErrored): string = +method `$`*(state: PurchaseErrored): string = "errored" + +method run*(state: PurchaseErrored, machine: Machine): Future[?State] {.async.} = + let purchase = Purchase(machine) + purchase.future.fail(state.error) diff --git a/codex/purchasing/states/errorhandling.nim b/codex/purchasing/states/errorhandling.nim new file mode 100644 index 00000000..57e00924 --- /dev/null +++ b/codex/purchasing/states/errorhandling.nim @@ -0,0 +1,9 @@ +import pkg/questionable +import ../statemachine +import ./error + +type + ErrorHandlingState* = ref object of PurchaseState + +method onError*(state: ErrorHandlingState, error: ref CatchableError): ?State = + some State(PurchaseErrored(error: error)) diff --git a/codex/purchasing/states/failed.nim b/codex/purchasing/states/failed.nim index 7f73104f..3fbe36f7 100644 --- a/codex/purchasing/states/failed.nim +++ b/codex/purchasing/states/failed.nim @@ -4,9 +4,9 @@ import ./error type PurchaseFailed* = ref object of PurchaseState -method enter*(state: PurchaseFailed) = - let error = newException(PurchaseError, "Purchase failed") - state.switch(PurchaseErrored(error: error)) - -method description*(state: PurchaseFailed): string = +method `$`*(state: PurchaseFailed): string = "failed" + +method run*(state: PurchaseFailed, machine: Machine): Future[?State] {.async.} = + let error = newException(PurchaseError, "Purchase failed") + return some State(PurchaseErrored(error: error)) diff --git a/codex/purchasing/states/finished.nim b/codex/purchasing/states/finished.nim index ce933207..93e8b4f0 100644 --- a/codex/purchasing/states/finished.nim +++ b/codex/purchasing/states/finished.nim @@ -2,11 +2,9 @@ import ../statemachine type PurchaseFinished* = ref object of PurchaseState -method enter*(state: PurchaseFinished) = - without purchase =? (state.context as Purchase): - raiseAssert "invalid state" - - purchase.future.complete() - -method description*(state: PurchaseFinished): string = +method `$`*(state: PurchaseFinished): string = "finished" + +method run*(state: PurchaseFinished, machine: Machine): Future[?State] {.async.} = + let purchase = Purchase(machine) + purchase.future.complete() diff --git a/codex/purchasing/states/pending.nim b/codex/purchasing/states/pending.nim index 8ade593c..92822ac7 100644 --- a/codex/purchasing/states/pending.nim +++ b/codex/purchasing/states/pending.nim @@ -1,21 +1,15 @@ import ../statemachine +import ./errorhandling import ./submitted import ./error -type PurchasePending* = ref object of PurchaseState +type PurchasePending* = ref object of ErrorHandlingState -method enterAsync(state: PurchasePending) {.async.} = - without purchase =? (state.context as Purchase) and - request =? purchase.request: - raiseAssert "invalid state" - - try: - await purchase.market.requestStorage(request) - except CatchableError as error: - state.switch(PurchaseErrored(error: error)) - return - - state.switch(PurchaseSubmitted()) - -method description*(state: PurchasePending): string = +method `$`*(state: PurchasePending): string = "pending" + +method run*(state: PurchasePending, machine: Machine): Future[?State] {.async.} = + let purchase = Purchase(machine) + let request = !purchase.request + await purchase.market.requestStorage(request) + return some State(PurchaseSubmitted()) diff --git a/codex/purchasing/states/started.nim b/codex/purchasing/states/started.nim index 439bc566..148ccc38 100644 --- a/codex/purchasing/states/started.nim +++ b/codex/purchasing/states/started.nim @@ -1,13 +1,16 @@ import ../statemachine +import ./errorhandling import ./error import ./finished import ./failed -type PurchaseStarted* = ref object of PurchaseState +type PurchaseStarted* = ref object of ErrorHandlingState -method enterAsync*(state: PurchaseStarted) {.async.} = - without purchase =? (state.context as Purchase): - raiseAssert "invalid state" +method `$`*(state: PurchaseStarted): string = + "started" + +method run*(state: PurchaseStarted, machine: Machine): Future[?State] {.async.} = + let purchase = Purchase(machine) let clock = purchase.clock let market = purchase.market @@ -18,17 +21,11 @@ method enterAsync*(state: PurchaseStarted) {.async.} = let subscription = await market.subscribeRequestFailed(purchase.requestId, callback) let ended = clock.waitUntil(await market.getRequestEnd(purchase.requestId)) - try: - let fut = await one(ended, failed) - if fut.id == failed.id: - ended.cancel() - state.switch(PurchaseFailed()) - else: - failed.cancel() - state.switch(PurchaseFinished()) - await subscription.unsubscribe() - except CatchableError as error: - state.switch(PurchaseErrored(error: error)) - -method description*(state: PurchaseStarted): string = - "started" + let fut = await one(ended, failed) + await subscription.unsubscribe() + if fut.id == failed.id: + ended.cancel() + return some State(PurchaseFailed()) + else: + failed.cancel() + return some State(PurchaseFinished()) diff --git a/codex/purchasing/states/submitted.nim b/codex/purchasing/states/submitted.nim index 9d5c8589..c051d282 100644 --- a/codex/purchasing/states/submitted.nim +++ b/codex/purchasing/states/submitted.nim @@ -1,15 +1,17 @@ import ../statemachine +import ./errorhandling import ./error import ./started import ./cancelled -type PurchaseSubmitted* = ref object of PurchaseState +type PurchaseSubmitted* = ref object of ErrorHandlingState -method enterAsync(state: PurchaseSubmitted) {.async.} = - without purchase =? (state.context as Purchase) and - request =? purchase.request: - raiseAssert "invalid state" +method `$`*(state: PurchaseSubmitted): string = + "submitted" +method run*(state: PurchaseSubmitted, machine: Machine): Future[?State] {.async.} = + let purchase = Purchase(machine) + let request = !purchase.request let market = purchase.market let clock = purchase.clock @@ -28,13 +30,6 @@ method enterAsync(state: PurchaseSubmitted) {.async.} = try: await wait().withTimeout() except Timeout: - state.switch(PurchaseCancelled()) - return - except CatchableError as error: - state.switch(PurchaseErrored(error: error)) - return + return some State(PurchaseCancelled()) - state.switch(PurchaseStarted()) - -method description*(state: PurchaseSubmitted): string = - "submitted" + return some State(PurchaseStarted()) diff --git a/codex/purchasing/states/unknown.nim b/codex/purchasing/states/unknown.nim index 6b66e964..cde4217a 100644 --- a/codex/purchasing/states/unknown.nim +++ b/codex/purchasing/states/unknown.nim @@ -1,4 +1,5 @@ import ../statemachine +import ./errorhandling import ./submitted import ./started import ./cancelled @@ -6,32 +7,26 @@ import ./finished import ./failed import ./error -type PurchaseUnknown* = ref object of PurchaseState +type PurchaseUnknown* = ref object of ErrorHandlingState -method enterAsync(state: PurchaseUnknown) {.async.} = - without purchase =? (state.context as Purchase): - raiseAssert "invalid state" - - try: - if (request =? await purchase.market.getRequest(purchase.requestId)) and - (requestState =? await purchase.market.requestState(purchase.requestId)): - - purchase.request = some request - - case requestState - of RequestState.New: - state.switch(PurchaseSubmitted()) - of RequestState.Started: - state.switch(PurchaseStarted()) - of RequestState.Cancelled: - state.switch(PurchaseCancelled()) - of RequestState.Finished: - state.switch(PurchaseFinished()) - of RequestState.Failed: - state.switch(PurchaseFailed()) - - except CatchableError as error: - state.switch(PurchaseErrored(error: error)) - -method description*(state: PurchaseUnknown): string = +method `$`*(state: PurchaseUnknown): string = "unknown" + +method run*(state: PurchaseUnknown, machine: Machine): Future[?State] {.async.} = + let purchase = Purchase(machine) + if (request =? await purchase.market.getRequest(purchase.requestId)) and + (requestState =? await purchase.market.requestState(purchase.requestId)): + + purchase.request = some request + + case requestState + of RequestState.New: + return some State(PurchaseSubmitted()) + of RequestState.Started: + return some State(PurchaseStarted()) + of RequestState.Cancelled: + return some State(PurchaseCancelled()) + of RequestState.Finished: + return some State(PurchaseFinished()) + of RequestState.Failed: + return some State(PurchaseFailed()) diff --git a/codex/rest/json.nim b/codex/rest/json.nim index 1a7941eb..ff087467 100644 --- a/codex/rest/json.nim +++ b/codex/rest/json.nim @@ -57,7 +57,7 @@ func `%`*(id: RequestId | SlotId | Nonce | AvailabilityId): JsonNode = func `%`*(purchase: Purchase): JsonNode = %*{ - "state": (purchase.state as PurchaseState).?description |? "none", + "state": purchase.state |? "none", "error": purchase.error.?msg, "request": purchase.request, } diff --git a/codex/utils/asyncstatemachine.nim b/codex/utils/asyncstatemachine.nim index 315ffc12..13392008 100644 --- a/codex/utils/asyncstatemachine.nim +++ b/codex/utils/asyncstatemachine.nim @@ -13,6 +13,7 @@ type scheduling: Future[void] started: bool State* = ref object of RootObj + Query*[T] = proc(state: State): T Event* = proc(state: State): ?State {.gcsafe, upraises:[].} logScope: @@ -26,6 +27,12 @@ proc transition(_: type Event, previous, next: State): Event = if state == previous: return some next +proc query*[T](machine: Machine, query: Query[T]): ?T = + if machine.state == nil: + none T + else: + some query(machine.state) + proc schedule*(machine: Machine, event: Event) = if not machine.started: return @@ -90,4 +97,5 @@ proc stop*(machine: Machine) = if not machine.running.isNil: machine.running.cancel() + machine.state = nil machine.started = false diff --git a/codex/utils/statemachine.nim b/codex/utils/statemachine.nim deleted file mode 100644 index 272dcb00..00000000 --- a/codex/utils/statemachine.nim +++ /dev/null @@ -1,130 +0,0 @@ -import std/typetraits -import pkg/chronicles -import pkg/questionable -import pkg/chronos -import ./optionalcast - -## Implementation of the the state pattern: -## https://en.wikipedia.org/wiki/State_pattern -## -## Define your own state machine and state types: -## -## type -## Light = ref object of StateMachine -## color: string -## LightState = ref object of State -## -## let light = Light(color: "yellow") -## -## Define the states: -## -## type -## On = ref object of LightState -## Off = ref object of LightState -## -## Perform actions on state entry and exit: -## -## method enter(state: On) = -## echo light.color, " light switched on" -## -## method exit(state: On) = -## echo light.color, " light no longer switched on" -## -## light.switch(On()) # prints: 'light switched on' -## light.switch(Off()) # prints: 'light no longer switched on' -## -## Allow behaviour to change based on the current state: -## -## method description*(state: LightState): string {.base.} = -## return "a light" -## -## method description*(state: On): string = -## if light =? (state.context as Light): -## return "a " & light.color & " light" -## -## method description*(state: Off): string = -## return "a dark light" -## -## proc description*(light: Light): string = -## if state =? (light.state as LightState): -## return state.description -## -## light.switch(On()) -## echo light.description # prints: 'a yellow light' -## light.switch(Off()) -## echo light.description # prints 'a dark light' - - -export questionable -export optionalcast - -type - StateMachine* = ref object of RootObj - state: ?State - State* = ref object of RootObj - context: ?StateMachine - -method `$`*(state: State): string {.base.} = - (typeof state).name - -method enter(state: State) {.base.} = - discard - -method exit(state: State) {.base.} = - discard - -func state*(machine: StateMachine): ?State = - machine.state - -func context*(state: State): ?StateMachine = - state.context - -proc switch*(machine: StateMachine, newState: State) = - if state =? machine.state: - state.exit() - state.context = StateMachine.none - machine.state = newState.some - newState.context = machine.some - newState.enter() - -proc switch*(oldState, newState: State) = - if context =? oldState.context: - context.switch(newState) - -type - AsyncState* = ref object of State - activeTransition: ?Future[void] - StateMachineAsync* = ref object of StateMachine - -method enterAsync(state: AsyncState) {.base, async.} = - discard - -method exitAsync(state: AsyncState) {.base, async.} = - discard - -method enter(state: AsyncState) = - asyncSpawn state.enterAsync() - -method exit(state: AsyncState) = - asyncSpawn state.exitAsync() - -proc switchAsync*(machine: StateMachineAsync, newState: AsyncState) {.async.} = - if state =? (machine.state as AsyncState): - trace "Switching sales state", `from` = $state, to = $newState - if activeTransition =? state.activeTransition and - not activeTransition.completed: - await activeTransition.cancelAndWait() - # should wait for exit before switch. could add a transition option during - # switch if we don't need to wait - await state.exitAsync() - state.context = none StateMachine - else: - trace "Switching state", `from` = "no state", to = $newState - - machine.state = some State(newState) - newState.context = some StateMachine(machine) - newState.activeTransition = some newState.enterAsync() - -proc switchAsync*(oldState, newState: AsyncState) {.async.} = - if context =? oldState.context: - await StateMachineAsync(context).switchAsync(newState) diff --git a/tests/codex/testpurchasing.nim b/tests/codex/testpurchasing.nim index 1944cae8..9b51673f 100644 --- a/tests/codex/testpurchasing.nim +++ b/tests/codex/testpurchasing.nim @@ -3,7 +3,12 @@ import pkg/asynctest import pkg/chronos import pkg/stint import pkg/codex/purchasing -import pkg/codex/purchasing/states/[finished, error, started, submitted, unknown] +import pkg/codex/purchasing/states/finished +import pkg/codex/purchasing/states/started +import pkg/codex/purchasing/states/submitted +import pkg/codex/purchasing/states/unknown +import pkg/codex/purchasing/states/cancelled +import pkg/codex/purchasing/states/failed import ./helpers/mockmarket import ./helpers/mockclock import ./helpers/eventually @@ -31,11 +36,11 @@ suite "Purchasing": test "submits a storage request when asked": discard await purchasing.purchase(request) - let submitted = market.requested[0] - check submitted.ask.slots == request.ask.slots - check submitted.ask.slotSize == request.ask.slotSize - check submitted.ask.duration == request.ask.duration - check submitted.ask.reward == request.ask.reward + check eventually market.requested.len > 0 + check market.requested[0].ask.slots == request.ask.slots + check market.requested[0].ask.slotSize == request.ask.slotSize + check market.requested[0].ask.duration == request.ask.duration + check market.requested[0].ask.reward == request.ask.reward test "remembers purchases": let purchase1 = await purchasing.purchase(request) @@ -49,11 +54,13 @@ suite "Purchasing": test "can change default value for proof probability": purchasing.proofProbability = 42.u256 discard await purchasing.purchase(request) + check eventually market.requested.len > 0 check market.requested[0].ask.proofProbability == 42.u256 test "can override proof probability per request": request.ask.proofProbability = 42.u256 discard await purchasing.purchase(request) + check eventually market.requested.len > 0 check market.requested[0].ask.proofProbability == 42.u256 test "has a default value for request expiration interval": @@ -63,25 +70,30 @@ suite "Purchasing": purchasing.requestExpiryInterval = 42.u256 let start = getTime().toUnix() discard await purchasing.purchase(request) + check eventually market.requested.len > 0 check market.requested[0].expiry == (start + 42).u256 test "can override expiry time per request": let expiry = (getTime().toUnix() + 42).u256 request.expiry = expiry discard await purchasing.purchase(request) + check eventually market.requested.len > 0 check market.requested[0].expiry == expiry test "includes a random nonce in every storage request": discard await purchasing.purchase(request) discard await purchasing.purchase(request) + check eventually market.requested.len > 0 check market.requested[0].nonce != market.requested[1].nonce test "sets client address in request": discard await purchasing.purchase(request) + check eventually market.requested.len > 0 check market.requested[0].client == await market.getSigner() test "succeeds when request is finished": let purchase = await purchasing.purchase(request) + check eventually market.requested.len > 0 let request = market.requested[0] let requestEnd = getTime().toUnix() + 42 market.requestEnds[request.id] = requestEnd @@ -92,6 +104,7 @@ suite "Purchasing": test "fails when request times out": let purchase = await purchasing.purchase(request) + check eventually market.requested.len > 0 let request = market.requested[0] clock.set(request.expiry.truncate(int64)) expect PurchaseTimeout: @@ -99,6 +112,7 @@ suite "Purchasing": test "checks that funds were withdrawn when purchase times out": let purchase = await purchasing.purchase(request) + check eventually market.requested.len > 0 let request = market.requested[0] clock.set(request.expiry.truncate(int64)) expect PurchaseTimeout: @@ -150,20 +164,20 @@ suite "Purchasing state machine": market.requestEnds[request2.id] = clock.now() - 1 await purchasing.load() - check purchasing.getPurchase(PurchaseId(request1.id)).?finished == false.some - check purchasing.getPurchase(PurchaseId(request2.id)).?finished == true.some - check purchasing.getPurchase(PurchaseId(request3.id)).?finished == true.some - check purchasing.getPurchase(PurchaseId(request4.id)).?finished == true.some - check purchasing.getPurchase(PurchaseId(request5.id)).?finished == true.some - check purchasing.getPurchase(PurchaseId(request5.id)).?error.isSome + check eventually purchasing.getPurchase(PurchaseId(request1.id)).?finished == false.some + check eventually purchasing.getPurchase(PurchaseId(request2.id)).?finished == true.some + check eventually purchasing.getPurchase(PurchaseId(request3.id)).?finished == true.some + check eventually purchasing.getPurchase(PurchaseId(request4.id)).?finished == true.some + check eventually purchasing.getPurchase(PurchaseId(request5.id)).?finished == true.some + check eventually purchasing.getPurchase(PurchaseId(request5.id)).?error.isSome test "moves to PurchaseSubmitted when request state is New": let request = StorageRequest.example let purchase = Purchase.new(request, market, clock) market.requested = @[request] market.requestState[request.id] = RequestState.New - purchase.switch(PurchaseUnknown()) - check (purchase.state as PurchaseSubmitted).isSome + let next = await PurchaseUnknown().run(purchase) + check !next of PurchaseSubmitted test "moves to PurchaseStarted when request state is Started": let request = StorageRequest.example @@ -171,69 +185,51 @@ suite "Purchasing state machine": market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64) market.requested = @[request] market.requestState[request.id] = RequestState.Started - purchase.switch(PurchaseUnknown()) - check (purchase.state as PurchaseStarted).isSome + let next = await PurchaseUnknown().run(purchase) + check !next of PurchaseStarted - test "moves to PurchaseErrored when request state is Cancelled": + test "moves to PurchaseCancelled when request state is Cancelled": let request = StorageRequest.example let purchase = Purchase.new(request, market, clock) market.requested = @[request] market.requestState[request.id] = RequestState.Cancelled - purchase.switch(PurchaseUnknown()) - check (purchase.state as PurchaseErrored).isSome - check purchase.error.?msg == "Purchase cancelled due to timeout".some + let next = await PurchaseUnknown().run(purchase) + check !next of PurchaseCancelled test "moves to PurchaseFinished when request state is Finished": let request = StorageRequest.example let purchase = Purchase.new(request, market, clock) market.requested = @[request] market.requestState[request.id] = RequestState.Finished - purchase.switch(PurchaseUnknown()) - check (purchase.state as PurchaseFinished).isSome + let next = await PurchaseUnknown().run(purchase) + check !next of PurchaseFinished - test "moves to PurchaseErrored when request state is Failed": + test "moves to PurchaseFailed when request state is Failed": let request = StorageRequest.example let purchase = Purchase.new(request, market, clock) market.requested = @[request] market.requestState[request.id] = RequestState.Failed - purchase.switch(PurchaseUnknown()) - check (purchase.state as PurchaseErrored).isSome - check purchase.error.?msg == "Purchase failed".some + let next = await PurchaseUnknown().run(purchase) + check !next of PurchaseFailed - test "moves to PurchaseErrored state once RequestFailed emitted": - let me = await market.getSigner() + test "moves to PurchaseFailed state once RequestFailed emitted": let request = StorageRequest.example - market.requested = @[request] - market.activeRequests[me] = @[request.id] - market.requestState[request.id] = RequestState.Started + let purchase = Purchase.new(request, market, clock) market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64) - await purchasing.load() + let future = PurchaseStarted().run(purchase) - # emit mock contract failure event market.emitRequestFailed(request.id) - # must allow time for the callback to trigger the completion of the future - await sleepAsync(chronos.milliseconds(10)) - # now check the result - let purchase = purchasing.getPurchase(PurchaseId(request.id)) - let state = purchase.?state - check (state as PurchaseErrored).isSome - check (!purchase).error.?msg == "Purchase failed".some + let next = await future + check !next of PurchaseFailed test "moves to PurchaseFinished state once request finishes": - let me = await market.getSigner() let request = StorageRequest.example - market.requested = @[request] - market.activeRequests[me] = @[request.id] - market.requestState[request.id] = RequestState.Started + let purchase = Purchase.new(request, market, clock) market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64) - await purchasing.load() + let future = PurchaseStarted().run(purchase) - # advance the clock to the end of the request clock.advance(request.ask.duration.truncate(int64)) - # now check the result - proc requestState: ?PurchaseState = - purchasing.getPurchase(PurchaseId(request.id)).?state as PurchaseState - - check eventually (requestState() as PurchaseFinished).isSome + let next = await future + check !next of PurchaseFinished diff --git a/tests/codex/testutils.nim b/tests/codex/testutils.nim index e6b885f7..f210ef1d 100644 --- a/tests/codex/testutils.nim +++ b/tests/codex/testutils.nim @@ -1,5 +1,3 @@ -import ./utils/teststatemachine -import ./utils/teststatemachineasync import ./utils/testoptionalcast import ./utils/testkeyutils import ./utils/testasyncstatemachine diff --git a/tests/codex/utils/testasyncstatemachine.nim b/tests/codex/utils/testasyncstatemachine.nim index d3032cbe..e8dbea8f 100644 --- a/tests/codex/utils/testasyncstatemachine.nim +++ b/tests/codex/utils/testasyncstatemachine.nim @@ -113,3 +113,21 @@ suite "async state machines": machine.schedule(moveToNextStateEvent) check eventually cancellations == [0, 1, 0, 0] check errors == [0, 0, 0, 0] + + test "queries properties of the current state": + proc description(state: State): string = + $state + + machine.start(State2.new()) + check eventually machine.query(description) == some "State2" + machine.schedule(moveToNextStateEvent) + check eventually machine.query(description) == some "State3" + + test "stops handling queries when stopped": + proc description(state: State): string = + $state + + machine.start(State2.new()) + check eventually machine.query(description).isSome + machine.stop() + check machine.query(description).isNone diff --git a/tests/codex/utils/teststatemachine.nim b/tests/codex/utils/teststatemachine.nim deleted file mode 100644 index 84a573c3..00000000 --- a/tests/codex/utils/teststatemachine.nim +++ /dev/null @@ -1,48 +0,0 @@ -import std/unittest -import pkg/questionable -import codex/utils/statemachine - -type - Light = ref object of StateMachine - On = ref object of State - Off = ref object of State - -var enteredOn: bool -var exitedOn: bool - -method enter(state: On) = - enteredOn = true - -method exit(state: On) = - exitedOn = true - -suite "state machines": - - setup: - enteredOn = false - exitedOn = false - - test "calls `enter` when entering state": - Light().switch(On()) - check enteredOn - - test "calls `exit` when exiting state": - let light = Light() - light.switch(On()) - check not exitedOn - light.switch(Off()) - check exitedOn - - test "allows access to state machine from state": - let light = Light() - let on = On() - check not isSome on.context - light.switch(on) - check on.context == some StateMachine(light) - - test "removes access to state machine when state exited": - let light = Light() - let on = On() - light.switch(on) - light.switch(Off()) - check not isSome on.context diff --git a/tests/codex/utils/teststatemachineasync.nim b/tests/codex/utils/teststatemachineasync.nim deleted file mode 100644 index 38431b41..00000000 --- a/tests/codex/utils/teststatemachineasync.nim +++ /dev/null @@ -1,30 +0,0 @@ -import pkg/asynctest -import pkg/chronos -import pkg/questionable -import codex/utils/statemachine - -type - AsyncMachine = ref object of StateMachineAsync - LongRunningStart = ref object of AsyncState - LongRunningFinish = ref object of AsyncState - LongRunningError = ref object of AsyncState - Callback = proc(): Future[void] {.gcsafe.} - -proc triggerIn(time: Duration, cb: Callback) {.async.} = - await sleepAsync(time) - await cb() - -method enterAsync(state: LongRunningStart) {.async.} = - proc cb() {.async.} = - await state.switchAsync(LongRunningFinish()) - asyncSpawn triggerIn(500.milliseconds, cb) - await sleepAsync(1.seconds) - await state.switchAsync(LongRunningError()) - -suite "async state machines": - - test "can cancel a state": - let am = AsyncMachine() - await am.switchAsync(LongRunningStart()) - await sleepAsync(2.seconds) - check (am.state as LongRunningFinish).isSome