import { createModel } from "@xstate/test" ; import { assertZWaveError } from "@zwave-js/core" ; import { assign , StateValue } from "xstate" ; import { interpret , Interpreter } from "xstate/lib/interpreter" ; import { createMachine } from "xstate/lib/Machine" ; import { BasicCCGet , BasicCCReport , BasicCCSet } from "../commandclass" ; import { ApplicationCommandRequest } from "../controller/ApplicationCommandRequest" ; import { GetControllerIdRequest , GetControllerIdResponse , } from "../controller/GetControllerIdMessages" ; import { SendDataRequest , SendDataRequestTransmitReport , } from "../controller/SendDataMessages" ; import { TransmitStatus } from "../controller/SendDataShared" ; import { MessagePriority } from "../message/Constants" ; import type { Message } from "../message/Message" ; import { createEmptyMockDriver } from "../test/mocks" ; import type { Driver } from "./Driver" ; import { createMessageGenerator } from "./MessageGenerators" ; import { createWrapperMachine } from "./StateMachineShared" ; import { Transaction } from "./Transaction" ; import { createTransactionMachine , TransactionMachineInterpreter , } from "./TransactionMachine" ; interface TestMachineContext { retryCount : number ; maxRetries : number ; } type TestMachineEvents = { type : "CREATE" ; target : string ; } | { type : "RETRY_TIMEOUT" ; } | { type : "TICK" ; } | { type : "SUCCESS_SIMPLE" ; } | { type : "FAILURE_SIMPLE" ; } | { type : "SUCCESS_SENDDATA" ; } | { type : "FAILURE_SENDDATA" ; } ; interface MockImplementations { notifyRetry : jest . Mock ; rejectTransaction : jest . Mock ; } interface TestContext { fakeDriver : Driver ; interpreter : TransactionMachineInterpreter ; testMessages : Record < string , Message > ; actualResult ? : Message ; expectedResult ? : Message ; actualError ? : Error ; expectedError ? : string ; forwardedMessages : Message [ ] ; transactionDone : boolean ; } jest . useFakeTimers ( ) ; function assertWrappedMachineState < T extends Interpreter < any , any , any , any > > ( interpreter , state ) { expect ( interpreter . children . get ( "child" ) ! . state . value ) . toEqual ( state ) ; } describe ( "lib/driver/TransactionMachine" , ( ) => { const testMachine = createMachine < TestMachineContext , TestMachineEvents > ( { id : "TransactionMachineTest" , initial : "init" , context : { retryCount : 0 , maxRetries : 0 } , states : { init : { on : { CREATE : [ { cond : ( _ , evt ) => evt . target === "execute_simple" , target : "execute_simple" } , { cond : ( _ , evt ) => evt . target === "execute_senddata" , target : "execute_senddata" , actions : assign ( { maxRetries : ( _ ) => 2 } ) } , ] } } , execute_simple : { on : { TICK : "respond_simple" } } , respond_simple : { on : { SUCCESS_SIMPLE : "finalize_simple" , FAILURE_SIMPLE : "finalize_simple" } , meta : { test : ( { interpreter , forwardedMessages , testMessages , } ) => { assertWrappedMachineState ( interpreter , "execute" ) ; expect ( forwardedMessages ) . toContain ( testMessages . GetControllerIdRequest ) ; } } } , finalize_simple : { on : { TICK : "done" } } , execute_senddata : { on : { TICK : "respond_senddata" } } , respond_senddata : { on : { SUCCESS_SENDDATA : "finalize_senddata" , FAILURE_SENDDATA : [ { cond : "mayRetry" , target : "retry_senddata" } , { target : "finalize_senddata" } , ] } , meta : { test : ( { interpreter , forwardedMessages , testMessages , } ) => { assertWrappedMachineState ( interpreter , "execute" ) ; expect ( forwardedMessages ) . toContain ( testMessages . BasicSet ) ; } } } , retry_senddata : { entry : assign ( { retryCount : ( ctx ) => ctx . retryCount + 1 } ) , on : { RETRY_TIMEOUT : "execute_senddata" } , meta : { test : ( { interpreter } ) => { assertWrappedMachineState ( interpreter , "retryWait" ) ; } } } , finalize_senddata : { on : { TICK : "done" } } , done : { meta : { test : ( { transactionDone , actualResult , expectedResult , actualError , expectedError , } ) => { if ( expectedResult ) { expect ( transactionDone ) . toBe ( true ) ; expect ( actualResult ) . toBe ( expectedResult ) ; } else if ( expectedError ) { assertZWaveError ( actualError , { messageMatches : expectedError } ) ; } } } } } } , { guards : { mayRetry : ( ctx ) => ctx . retryCount < ctx . maxRetries } } ) ; const testModel = createModel < TestContext , TestMachineContext > ( testMachine ) . withEvents ( { CREATE : { cases : [ { message : "GetControllerIdRequest" , target : "execute_simple" } , { message : "BasicSet" , target : "execute_senddata" } , { message : "BasicGet" , target : "execute_get" } , ] } , TICK : { exec : ( ) => { jest . advanceTimersByTime ( 1 ) ; } } , RETRY_TIMEOUT : { exec : ( ) => { jest . advanceTimersByTime ( 500 ) ; } } , SUCCESS_SIMPLE : { exec : ( context ) => { const { interpreter , testMessages } = context ; const result = testMessages . GetControllerIdResponse ; context . expectedResult = result ; interpreter . send ( { type : "command_success" , result } as any ) ; } } , FAILURE_SIMPLE : { exec : ( context ) => { const { interpreter } = context ; context . expectedResult = undefined ; context . expectedError = "Timeout while waiting for an ACK" ; interpreter . send ( { type : "command_failure" , reason : "ACK timeout" } as any ) ; } } , SUCCESS_SENDDATA : { exec : ( context ) => { const { interpreter , forwardedMessages , fakeDriver } = context ; const sentMessage = forwardedMessages [ forwardedMessages . length - 1 ] ; const result = new SendDataRequestTransmitReport ( fakeDriver , { callbackId : sentMessage . callbackId , transmitStatus : TransmitStatus . OK } ) ; context . expectedResult = result ; interpreter . send ( { type : "command_success" , result } as any ) ; } } , FAILURE_SENDDATA : { exec : ( context ) => { const { interpreter } = context ; context . expectedResult = undefined ; context . expectedError = "Failed to send" ; interpreter . send ( { type : "command_failure" , reason : "response NOK" } as any ) ; } } } ) ; const testPlans = testModel . getSimplePathPlans ( ) ; testPlans . forEach ( ( plan ) => { if ( plan . state . value === "init" ) return ; const planDescription = plan . description . replace ( ` (${ JSON . stringify ( plan . state . context ) })` , "" ) ; describe ( planDescription , ( ) => { plan . paths . forEach ( ( path ) => { it ( path . description , ( ) => { let context ; const createMachineRegex = /CREATE \((?<json>[^\)]+)\)/ ; const match = createMachineRegex . exec ( path . description ) ; if ( ! match ?. groups ?. json ) return path . test ( undefined as any ) ; const fakeDriver = createEmptyMockDriver ( ) as unknown as Driver ; const ctrlrIdRequest = new GetControllerIdRequest ( fakeDriver ) ; const ctrlrIdResponse = new GetControllerIdResponse ( fakeDriver , { data : Buffer . from ( "01080120dc3452b301de" , "hex" ) } ) ; const sendDataBasicGet = new SendDataRequest ( fakeDriver , { command : new BasicCCGet ( fakeDriver , { nodeId : 2 } ) } ) ; const sendDataBasicReport = new ApplicationCommandRequest ( fakeDriver , { command : new BasicCCReport ( fakeDriver , { nodeId : 2 , currentValue : 50 } ) } ) ; const sendDataBasicSet = new SendDataRequest ( fakeDriver , { command : new BasicCCSet ( fakeDriver , { nodeId : 2 , targetValue : 22 } ) } ) ; const testMessages = { GetControllerIdRequest : ctrlrIdRequest , GetControllerIdResponse : ctrlrIdResponse , BasicSet : sendDataBasicSet , BasicGet : sendDataBasicGet , BasicReport : sendDataBasicReport } ; const testCase = JSON . parse ( match . groups . json ) ; const message = ( testMessages as any ) [ testCase . message ] as Message ; const { generator , resultPromise } = createMessageGenerator ( fakeDriver , message , ( ) => void 0 ) ; const transaction = new Transaction ( fakeDriver , { message , parts : generator , promise : resultPromise , priority : MessagePriority . Normal } ) ; const implementations = { notifyRetry : jest . fn ( ) , rejectTransaction : jest . fn ( ) . mockImplementation ( ( t , e ) => { resultPromise . reject ( e ) ; } ) } ; const machine = createTransactionMachine ( "T1" , transaction , implementations as any ) ; const wrapper = createWrapperMachine ( machine ) ; context = { fakeDriver , interpreter : interpret ( wrapper ) , testMessages , forwardedMessages : [ ] , transactionDone : false } ; context . interpreter . onEvent ( ( evt ) => { if ( evt . type === "forward" && evt . to === "QUEUE" ) { context . forwardedMessages . push ( evt . payload . transaction . message ) ; } else if ( evt . type === "transaction_done" ) { context . transactionDone = true ; } } ) ; resultPromise . then ( ( result ) => { context . actualResult = result ; } ) . catch ( ( err ) => { context . actualError = err ; } ) ; context . interpreter . start ( ) ; return path . test ( context ) ; } ) ; } ) ; } ) ; } ) ; it ( "coverage" , ( ) => { testModel . testCoverage ( { filter : ( stateNode ) => { return ! ! stateNode . meta ; } } ) ; } ) ; } ) ;