Tuesday, October 09, 2012

Testing MFlow applications

MFlow is a web application server for stateful type safe interactions with Web users. Because the interaction is done trough a single primitive, ask,  It is possible to replace this primitive by a injector of user inputs with the same name. Because the ask channel is typed, I can define a generator. The result is a very simple simulation of user sessions that can be used to generate different load conditions, to discover bottlenecks, bugs etc.

I define the class Response that generate valid responses for a type a :

class Response a where
  response :: IO a

I use the  Random instance of Int as the generator

instance Response a => Response (Maybe a) where
   response= do
     b <- randomRIO(0,1 :: Int)
     case b of 0 -> response >>= return . Just ; _ -> return Nothing

instance  Response String where
   response= replicateM 5  $ randomRIO ('a','z')
   
instance Response Int where
   response= randomRIO(1,1000)

instance Response Integer where
   response= randomRIO(1,1000)

instance (Response a, Response b) => Response (a,b) where
  response= fmap (,) response `ap` response

For enumerable types I use Bounded and Enum.

instance (Bounded a, Enum a) => Response a where
    response= mx
     where
     mx= do
          let x= typeOfIO mx

          n <- randomRIO ( fromEnum $ minBound `asTypeOf` x
                         , fromEnum $ maxBound `asTypeOf` x)
          return $ toEnum n
          where
          typeOfIO :: IO a -> a
          typeOfIO = error $ "typeOfIO not defined"

With these instances I can redefine ask, which takes a widget in the View applicative and generates a response in the FlowM monad:

ask :: (Response a) => View v m a -> FlowM v m a
ask w = do
     w  `MFlow.Forms.wmodify` (\v x -> consume v >> return (v,x))
     `seq` rest
     where
     consume= liftIO . B.writeFile "dev/null" . B.concat . map  toByteString
     rest= do       
        bool <- liftIO $ response
        case bool of
              False -> fail ""
              True -> do
                b <- liftIO response
                r <- liftIO response
                case  (b,r)  of
                    (True,x)  -> breturn x
                    _         -> ask w
     
It executes the widget rendering code and simulate the conplete pass trough a ByteString channel by sending the result to /dev/null.  Then he uses the Response instance to return a result to the flow. Simulation of the back button is possible (by fail)  and also failed validations are simulated, which invokes ask again (at the end)

There are however callbacks and modifiers that do something with re result before returning to the main flow. To take care of it, waction and wmodify have also tweaks that can simulate valid entry values for them.

The resulting code is here: MFlow.Forms.Test

Let's use it . This example below creates 15 thread and invoke the shopCart procedure, which is persistent (it remenber the state when it restart, it is a nice feature of MFlow).

runTest is the primitive that spawn the threads and invoke the flows:

test= do
   addMessageFlows [("shop"    ,runFlow shopCart)]
   runTest [(15, "shop")]

data ShopOptions= IPhone | IPod | IPad deriving (Bounded, Enum,Read, Show, Typeable)

-- A persistent flow  (uses step). The process is killed after 10 seconds of inactivity
-- but it is restarted automatically. if you restart the program, it remember the shopping cart
-- defines a table with links enclosed that return an user defined type.
shopCart  = do
   setTimeouts 10 0
   shopCart1 (V.fromList [0,0,0:: Int])
   where
   shopCart1 cart=  do
     o <- step . ask $
             table ! [border 1,thestyle "width:20%;margin-left:auto;margin-right:auto"]
             <<< caption << "choose an item"
             ++> thead << tr << concatHtml[ th << bold << "item", th << bold << "times chosen"]
             ++> (tbody
                  <<<  tr ! [rowspan 2] << td << linkHome
                  ++> (tr <<< td <<< wlink IPhone (bold <<"iphone") <++  td << ( bold << show ( cart V.! 0))
                  <|>  tr <<< td <<< wlink IPad (bold <<"ipad")   <++  td << ( bold << show ( cart V.! 1))
                  <|>  tr <<< td <<< wlink IPod (bold <<"ipod")   <++  td << ( bold << show ( cart V.! 2)))
                  )
     let i =fromEnum o
     let newCart= cart V.// [(i, cart V.!  i + 1 )]
     shopCart1 newCart
     
    where
    linkHome= (toHtml $ hotlink  noScript << bold << "home")

When executed properly this example iterates to fill a simple shopping cart from the user input. But with the test we can check the application in different load conditions, so we can discover bottlenecks, bugs, performance issues and other interesting things.

The complete code of the example is at the demos.hs in the Git repository.

This is a (reduced) flow log of one of these 15 threads:

3729 178                                        
 [ "()   "
 , "B IPad   " 
 , "G  " 
 , "B IPad   " 
 , "G  " 
 , "G  " 
 , "B IPod   " 
 , "G  " 
 , "G  " 
 , "B IPad   " 
 , "G  " 
 , "B IPhone   " 
 , "G  " 
 , "B IPad   " 
 , "B IPhone   " 
 ] 
 Stat "pkpvo/void"  178  ( Nothing ) 0  

"G "  means that the generator has selected the back button.

To summarize, the stateful , typed nature and the simplicity of the MFlow user interface makes the test infrastructure very simple and powerful.  And this is the beginning.