On Wed, Feb 06, 2013 at 01:09:51PM +0100, Michele Tartara wrote:
> In order to fetch precise information about the status of the VMs running in
> Xen, we need to analyze the output of the "xm list --long" command.
> 
> This commit adds the parser to do that, and its tests.
> 
> Signed-off-by: Michele Tartara <[email protected]>
> ---
>  Makefile.am                                    |   10 ++
>  src/Ganeti/Hypervisor/Xen/Types.hs             |  107 +++++++++++++++++++
>  src/Ganeti/Hypervisor/Xen/XmParser.hs          |  132 +++++++++++++++++++++++
>  test/data/xen-xm-list-long-4.0.1.txt           |  134 
> ++++++++++++++++++++++++
>  test/hs/Test/Ganeti/Hypervisor/Xen/XmParser.hs |  119 +++++++++++++++++++++
>  test/hs/htest.hs                               |    2 +
>  6 files changed, 504 insertions(+)
>  create mode 100644 src/Ganeti/Hypervisor/Xen/Types.hs
>  create mode 100644 src/Ganeti/Hypervisor/Xen/XmParser.hs
>  create mode 100644 test/data/xen-xm-list-long-4.0.1.txt
>  create mode 100644 test/hs/Test/Ganeti/Hypervisor/Xen/XmParser.hs
> 
> diff --git a/Makefile.am b/Makefile.am
> index b1cbdac..9577f53 100644
> --- a/Makefile.am
> +++ b/Makefile.am
> @@ -65,6 +65,8 @@ HS_DIRS = \
>       src/Ganeti/HTools \
>       src/Ganeti/HTools/Backend \
>       src/Ganeti/HTools/Program \
> +     src/Ganeti/Hypervisor \
> +     src/Ganeti/Hypervisor/Xen \
>       src/Ganeti/Query \
>       test/hs \
>       test/hs/Test \
> @@ -74,6 +76,8 @@ HS_DIRS = \
>       test/hs/Test/Ganeti/Confd \
>       test/hs/Test/Ganeti/HTools \
>       test/hs/Test/Ganeti/HTools/Backend \
> +     test/hs/Test/Ganeti/Hypervisor \
> +     test/hs/Test/Ganeti/Hypervisor/Xen \
>       test/hs/Test/Ganeti/Query
>  
>  DIRS = \
> @@ -121,6 +125,8 @@ ALL_APIDOC_HS_DIRS = \
>       $(APIDOC_HS_DIR)/Ganeti/HTools \
>       $(APIDOC_HS_DIR)/Ganeti/HTools/Backend \
>       $(APIDOC_HS_DIR)/Ganeti/HTools/Program \
> +     $(APIDOC_HS_DIR)/Ganeti/Hypervisor \
> +     $(APIDOC_HS_DIR)/Ganeti/Hypervisor/Xen \
>       $(APIDOC_HS_DIR)/Ganeti/Query
>  
>  BUILDTIME_DIR_AUTOCREATE = \
> @@ -514,6 +520,8 @@ HS_LIB_SRCS = \
>       src/Ganeti/HTools/Program/Hroller.hs \
>       src/Ganeti/HTools/Program/Main.hs \
>       src/Ganeti/HTools/Types.hs \
> +     src/Ganeti/Hypervisor/Xen/XmParser.hs \
> +     src/Ganeti/Hypervisor/Xen/Types.hs \
>       src/Ganeti/Hash.hs \
>       src/Ganeti/JQueue.hs \
>       src/Ganeti/JSON.hs \
> @@ -563,6 +571,7 @@ HS_TEST_SRCS = \
>       test/hs/Test/Ganeti/HTools/Node.hs \
>       test/hs/Test/Ganeti/HTools/PeerMap.hs \
>       test/hs/Test/Ganeti/HTools/Types.hs \
> +     test/hs/Test/Ganeti/Hypervisor/Xen/XmParser.hs \
>       test/hs/Test/Ganeti/JSON.hs \
>       test/hs/Test/Ganeti/Jobs.hs \
>       test/hs/Test/Ganeti/JQueue.hs \
> @@ -1044,6 +1053,7 @@ TEST_FILES = \
>       test/data/xen-xm-info-4.0.1.txt \
>       test/data/xen-xm-list-4.0.1-dom0-only.txt \
>       test/data/xen-xm-list-4.0.1-four-instances.txt \
> +     test/data/xen-xm-list-long-4.0.1.txt \
>       test/py/ganeti-cli.test \
>       test/py/gnt-cli.test \
>       test/py/import-export_unittest-helper
> diff --git a/src/Ganeti/Hypervisor/Xen/Types.hs 
> b/src/Ganeti/Hypervisor/Xen/Types.hs
> new file mode 100644
> index 0000000..ec1049f
> --- /dev/null
> +++ b/src/Ganeti/Hypervisor/Xen/Types.hs
> @@ -0,0 +1,107 @@
> +{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
> +{-| Data types for Xen-specific hypervisor functionalities.
> +
> +-}
> +{-
> +
> +Copyright (C) 2013 Google Inc.
> +
> +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 2 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, write to the Free Software
> +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +02110-1301, USA.
> +
> +-}
> +module Ganeti.Hypervisor.Xen.Types
> +  ( LispConfig(..)
> +  , Domain(..)
> +  , FromLispConfig(..)
> +  , ActualState(..)
> +  ) where
> +
> +import qualified Text.JSON as J
> +
> +import Ganeti.BasicTypes
> +
> +-- | Data type representing configuration data as produced by the
> +-- @xm list --long@ command.
> +data LispConfig = LCList [LispConfig]
> +            | LCString String
> +            | LCDouble Double
> +            deriving (Eq, Show)

FYI, usually the intending aligns the '|' with the '=', so that all
constructors start in the same column.

> +
> +-- | Data type representing a Xen Domain.
> +data Domain = Domain
> +  { domId :: Int
> +  , domName :: String
> +  , domCpuTime :: Double
> +  , domState :: ActualState
> +  , domIsHung :: Maybe Bool
> +  } deriving (Show, Eq)

(I) Usually also align on '::' as well.

> +-- | Class representing all the types that can be extracted from LispConfig.
> +class FromLispConfig a where
> +  fromLispConfig :: LispConfig -> Result a
> +
> +-- | Instance of FromLispConfig for Int.
> +instance FromLispConfig Int where
> +  fromLispConfig (LCDouble d) = Ok $ floor d
> +  fromLispConfig (LCList (LCString _:LCDouble d:[])) = Ok $ floor d
> +  fromLispConfig c =
> +    Bad $ "Unable to extract a Int from this configuration: "
> +      ++ show c
> +
> +-- | Instance of FromLispConfig for Double.
> +instance FromLispConfig Double where
> +  fromLispConfig (LCDouble d) = Ok d
> +  fromLispConfig (LCList (LCString _:LCDouble d:[])) = Ok d
> +  fromLispConfig c =
> +    Bad $ "Unable to extract a Double from this configuration: "
> +      ++ show c
> +
> +-- | Instance of FromLispConfig for String

.

> +instance FromLispConfig String where
> +  fromLispConfig (LCString s) = Ok s
> +  fromLispConfig (LCList (LCString _:LCString s:[])) = Ok s
> +  fromLispConfig c =
> +    Bad $ "Unable to extract a String from this configuration: "
> +      ++ show c
> +
> +-- | Instance of FromLispConfig for [LispConfig]

.

> +instance FromLispConfig [LispConfig] where
> +  fromLispConfig (LCList l) = Ok l
> +  fromLispConfig c =
> +    Bad $ "Unable to extract a List from this configuration: "
> +      ++ show c
> +
> +data ActualState = ActualRunning  -- The instance is running
> +                 | ActualBlocked  -- The instance is not running or runnable
> +                 | ActualPaused   -- The instance has been paused
> +                 | ActualShutdown -- The instance is shut down
> +                 | ActualCrashed  -- The instance has crashed
> +                 | ActualDying    -- The instance is in process of dying
> +                 | ActualHung     -- The instance is hung
> +                 | ActualUnknown  -- Unknown state. Parsing error.
> +                 deriving (Show, Eq)

You're missing '^'. The correct syntax is 'ActualRunning -- ^ The
instance is running'.

> +instance J.JSON ActualState where
> +  showJSON ActualRunning = J.showJSON "running"
> +  showJSON ActualBlocked = J.showJSON "blocked"
> +  showJSON ActualPaused = J.showJSON "paused"
> +  showJSON ActualShutdown = J.showJSON "shutdown"
> +  showJSON ActualCrashed = J.showJSON "crashed"
> +  showJSON ActualDying = J.showJSON "dying"
> +  showJSON ActualHung = J.showJSON "hung"
> +  showJSON ActualUnknown = J.showJSON "unknown"
> +
> +  readJSON = error "JSON read instance not implemented for type ActualState"
> diff --git a/src/Ganeti/Hypervisor/Xen/XmParser.hs 
> b/src/Ganeti/Hypervisor/Xen/XmParser.hs
> new file mode 100644
> index 0000000..c037910
> --- /dev/null
> +++ b/src/Ganeti/Hypervisor/Xen/XmParser.hs
> @@ -0,0 +1,132 @@
> +{-# LANGUAGE OverloadedStrings, FlexibleInstances #-}
> +{-| Parser for the output of the @xm list --long@ command of Xen
> +
> +-}
> +{-
> +
> +Copyright (C) 2013 Google Inc.
> +
> +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 2 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, write to the Free Software
> +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +02110-1301, USA.
> +
> +-}
> +module Ganeti.Hypervisor.Xen.XmParser
> +  ( xmListParser
> +  , lispConfigParser
> +  ) where
> +
> +import Control.Applicative ((*>), (<*), (<|>), pure)
> +import Control.Monad
> +import qualified Data.Attoparsec.Text as A
> +import qualified Data.Attoparsec.Combinator as AC
> +import Data.Attoparsec.Text (Parser)
> +import Data.Char (isSpace)
> +import Data.List
> +import qualified Data.Map as Map
> +import Data.Text (unpack)
> +
> +import Ganeti.BasicTypes
> +import Ganeti.Hypervisor.Xen.Types
> +
> +
> +-- | A parser for parsing generic config files written in the (LISP-like)
> +-- format that is the output of the @xm list --long@ command.
> +-- This parser only takes care of the syntactic parse, but does not care
> +-- about the semantics.
> +lispConfigParser :: Parser LispConfig
> +lispConfigParser =
> +  A.skipSpace *>
> +    (   listConfigP
> +    <|> doubleP
> +    <|> stringP
> +    )
> +  <* A.skipSpace
> +    where listConfigP =
> +            (A.char '(' *> lispConfigParser `AC.manyTill` A.char ')')
> +              >>= pure . LCList
> +          doubleP = A.double >>= pure . LCDouble
> +          stringP =
> +            A.takeWhile1 (
> +              \c -> (not . isSpace) c
> +                    && (c /= ')')
> +                    && (c /= '(')
> +            ) >>= (pure . LCString . unpack )
> +
> +-- | Find a configuration having the given string as its first element,
> +-- from a list of configurations.
> +findConf :: String -> [LispConfig] -> Result LispConfig
> +findConf key configs =
> +  case find (isNamed key) configs of
> +    (Just c) -> Ok c
> +    _ -> Bad "Configuration not found"
> +
> +-- | Get the value of of a configuration having the given string as its
> +-- first element.
> +-- The value is the content of the configuration, discarding the name itself.
> +getValue :: (FromLispConfig a) => String -> [LispConfig] -> Result a
> +getValue key configs = findConf key configs >>= fromLispConfig
> +
> +-- | Extract the values of a configuration containing a list of them.
> +extractValues :: LispConfig -> Result [LispConfig]
> +extractValues c = tail `fmap` fromLispConfig c
> +
> +-- | Verify whether the given configuration has a certain name or not.fmap
> +-- The name of a configuration is its first parameter, if it is a string.
> +isNamed :: String -> LispConfig -> Bool
> +isNamed key (LCList (LCString x:_)) = x == key
> +isNamed _ _ = False
> +
> +-- | Parser for recognising the current state of a Xen domain.
> +parseState :: String -> ActualState
> +parseState s =
> +  case s of
> +    "r-----" -> ActualRunning
> +    "-b----" -> ActualBlocked
> +    "--p---" -> ActualPaused
> +    "---s--" -> ActualShutdown
> +    "----c-" -> ActualCrashed
> +    "-----d" -> ActualDying
> +    _ -> ActualUnknown

Coming back to our previous discussion about ActualUnknown: the only
thing I don't like is that ActualUnknown right now "swallows" and hides
the error message. OK for now, but it would be better to have it as an
argument to the ActualUnknown constructor.

> +-- | Extract the configuration data of a Xen domain from a generic LispConfig
> +-- data structure. Fail if the LispConfig does not represent a domain.
> +getDomainConfig :: LispConfig -> Result Domain
> +getDomainConfig configData = do
> +  domainConf <-
> +    if isNamed "domain" configData
> +      then extractValues configData
> +      else Bad $ "Not a domain configuration: " ++ show configData
> +  domid <- getValue "domid" domainConf
> +  name <- getValue "name" domainConf
> +  cpuTime <- getValue "cpu_time" domainConf
> +  state <- getValue "state" domainConf
> +  let actualState = parseState state
> +  return $ Domain domid name cpuTime actualState Nothing
> +
> +-- | A parser for parsing the output of the @xm list --long@ command.
> +-- It adds the semantic layer on top of lispConfigParser.
> +-- It returns a map of domains, with their name as the key.
> +-- FIXME: This is efficient under the assumption that only a few fields of 
> the
> +-- domain configuration are actually needed. If many of them are required, a
> +-- parser able to directly extract the domain config would actually be 
> better.
> +xmListParser :: Parser (Map.Map String Domain)
> +xmListParser = do
> +  configs <- lispConfigParser `AC.manyTill` A.endOfInput
> +  let domains = map getDomainConfig configs
> +      foldResult m (Ok val) = Ok $ Map.insert (domName val) val m
> +      foldResult _ (Bad msg) = Bad msg
> +  case foldM foldResult Map.empty domains of
> +    Ok d -> return d
> +    Bad msg -> fail msg
> diff --git a/test/data/xen-xm-list-long-4.0.1.txt 
> b/test/data/xen-xm-list-long-4.0.1.txt
> new file mode 100644
> index 0000000..404e98b
> --- /dev/null
> +++ b/test/data/xen-xm-list-long-4.0.1.txt
> @@ -0,0 +1,134 @@
> +(domain
> +    (domid 0)
> +    (cpu_weight 2048)
> +    (cpu_cap 0)
> +    (bootloader )
> +    (on_crash restart)
> +    (uuid 00000000-0000-0000-0000-000000000000)
> +    (bootloader_args )
> +    (vcpus 24)
> +    (name Domain-0)
> +    (cpus
> +        ((0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +            (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
> +        )
> +    )
> +    (on_reboot restart)
> +    (on_poweroff destroy)
> +    (maxmem 16777215)
> +    (memory 1023)
> +    (shadow_memory 0)
> +    (features )
> +    (on_xend_start ignore)
> +    (on_xend_stop ignore)
> +    (cpu_time 184000.41332)
> +    (online_vcpus 1)
> +    (image (linux (kernel ) (superpages 0) (nomigrate 0) (tsc_mode 0)))
> +    (status 2)
> +    (state r-----)
> +)
> +(domain
> +    (domid 119)
> +    (cpu_weight 256)
> +    (cpu_cap 0)
> +    (bootloader )
> +    (on_crash restart)
> +    (uuid e430b4b8-dc91-9390-dfe0-b83c138ea0aa)
> +    (bootloader_args )
> +    (vcpus 1)
> +    (description )
> +    (name instance1.example.com)
> +    (cpus (()))
> +    (on_reboot restart)
> +    (on_poweroff destroy)
> +    (maxmem 128)
> +    (memory 128)
> +    (shadow_memory 0)
> +    (features )
> +    (on_xend_start ignore)
> +    (on_xend_stop ignore)
> +    (start_time 1357749308.05)
> +    (cpu_time 24.116146647)
> +    (online_vcpus 1)
> +    (image
> +        (linux
> +            (kernel /boot/vmlinuz-ganetixenu)
> +            (args 'root=/dev/xvda1 ro')
> +            (superpages 0)
> +            (videoram 4)
> +            (pci ())
> +            (nomigrate 0)
> +            (tsc_mode 0)
> +            (notes
> +                (HV_START_LOW 18446603336221196288)
> +                (FEATURES '!writable_page_tables|pae_pgdir_above_4gb')
> +                (VIRT_BASE 18446744071562067968)
> +                (GUEST_VERSION 2.6)
> +                (PADDR_OFFSET 0)
> +                (GUEST_OS linux)
> +                (HYPERCALL_PAGE 18446744071578849280)
> +                (LOADER generic)
> +                (SUSPEND_CANCEL 1)
> +                (PAE_MODE yes)
> +                (ENTRY 18446744071592116736)
> +                (XEN_VERSION xen-3.0)
> +            )
> +        )
> +    )
> +    (status 2)
> +    (state -b----)
> +    (store_mfn 8836555)
> +    (console_mfn 8735251)
> +    (device
> +        (vif
> +            (bridge xen-br0)
> +            (mac aa:00:00:30:8d:9d)
> +            (script /etc/xen/scripts/vif-bridge)
> +            (uuid f57c4758-cf0a-8227-6d13-fe26ece82d75)
> +            (backend 0)
> +        )
> +    )
> +    (device
> +        (console
> +            (protocol vt100)
> +            (location 2)
> +            (uuid 7695737a-ffc2-4e0d-7f6d-734143b8afc4)
> +        )
> +    )
> +    (device
> +        (vbd
> +            (protocol x86_64-abi)
> +            (uuid 409e1ff8-435a-4704-80bb-4bfe800d932e)
> +            (bootable 1)
> +            (dev sda:disk)
> +            (uname
> +                phy:/var/run/ganeti/instance-disks/instance1.example.com:0
> +            )
> +            (mode w)
> +            (backend 0)
> +            (VDI )
> +        )
> +    )
> +)
> diff --git a/test/hs/Test/Ganeti/Hypervisor/Xen/XmParser.hs 
> b/test/hs/Test/Ganeti/Hypervisor/Xen/XmParser.hs
> new file mode 100644
> index 0000000..e781d8b
> --- /dev/null
> +++ b/test/hs/Test/Ganeti/Hypervisor/Xen/XmParser.hs
> @@ -0,0 +1,119 @@
> +{-# LANGUAGE TemplateHaskell #-}
> +{-# OPTIONS_GHC -fno-warn-orphans #-}
> +
> +{-| Unittests for @xm list --long@ parser -}
> +
> +{-
> +
> +Copyright (C) 2013 Google Inc.
> +
> +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 2 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, write to the Free Software
> +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +02110-1301, USA.
> +
> +-}
> +
> +module Test.Ganeti.Hypervisor.Xen.XmParser
> +  ( testHypervisor_Xen_XmParser
> +  ) where
> +
> +import Test.HUnit
> +import Test.QuickCheck as QuickCheck hiding (Result)
> +
> +import Test.Ganeti.TestHelper
> +import Test.Ganeti.TestCommon
> +
> +import Control.Monad (liftM)
> +import qualified Data.Attoparsec.Text as A
> +import Data.Char (isAsciiUpper, isAsciiLower)
> +import qualified Data.Map as Map
> +import Data.Text (pack)
> +
> +import Ganeti.Hypervisor.Xen.Types
> +import Ganeti.Hypervisor.Xen.XmParser
> +{-# ANN module "HLint: ignore Use camelCase" #-}

Please one blank line before the annotation.
> +
> +-- | Generates an arbitrary non-empty string

(.)

> +-- Note that isAlpha is not used because it could allow the generation of
> +-- unicode characters, that we do not want.
> +nonEmptyString :: Gen String
> +nonEmptyString = listOf1 $
> +  arbitrary `suchThat` (\c -> isAsciiUpper c || isAsciiLower c)

Same comment as on the other patch.

> +-- * Arbitraries
> +
> +-- | Arbitrary instance for generating configurations.
> +-- A completely arbitrary configuration would contain too many lists and its
> +-- size would be to big to be actually parsable in reasonable time.
> +-- This Arbitrary builds a random Config that is still of a reasonable size.
> +instance Arbitrary LispConfig where
> +  arbitrary = frequency
> +    [ (5, liftM LCString nonEmptyString)
> +    , (5, liftM LCDouble arbitrary)
> +    , (1, liftM LCList (choose(1,20) >>= (`vectorOf` arbitrary)))
> +    ]
> +
> +-- * Helper functions for tests
> +
> +-- | Function for testing whether a domain configuration is parsed correctly.
> +testDomain :: String -> Map.Map String Domain -> Assertion
> +testDomain fileName expectedContent = do
> +    fileContent <- readTestData fileName
> +    case A.parseOnly xmListParser $ pack fileContent of
> +        Left msg -> assertFailure $ "Parsing failed: " ++ msg
> +        Right obtained -> assertEqual fileName expectedContent obtained
> +
> +-- | Determines whether two LispConfig are equal, with the exception of 
> Double
> +-- values, that just need to be "almost equal".
> +-- Meant mainly for testing purposes, given that Double values may be 
> slightly
> +-- rounded during parsing.
> +isAlmostEqual :: LispConfig -> LispConfig -> Bool
> +isAlmostEqual (LCList c1) (LCList c2) =
> +  (length c1 == length c2) &&
> +  foldr
> +    (\current acc -> (acc && uncurry isAlmostEqual current))
> +    True
> +    (zip c1 c2)
> +isAlmostEqual (LCString s1) (LCString s2) = s1 == s2
> +isAlmostEqual (LCDouble d1) (LCDouble d2) = abs (d1-d2) <= 1e-12
> +isAlmostEqual _ _ = False
> +
> +-- | Function to serialize LispConfigs in such a way that they can be rebuilt
> +-- again by the lispConfigParser.
> +serializeConf :: LispConfig -> String
> +serializeConf (LCList c) = "(" ++ unwords (map serializeConf c) ++ ")"
> +serializeConf (LCString s) = s
> +serializeConf (LCDouble d) = show d
> +
> +-- | Test whether a randomly generated config can be parsed.
> +-- Implicitly, this also tests that the Show instance of Config is correct.
> +prop_config :: LispConfig -> Property
> +prop_config conf =
> +  case A.parseOnly lispConfigParser . pack . serializeConf $ conf of
> +        Left msg -> fail $ "Parsing failed: " ++ msg
> +        Right obtained -> property $ isAlmostEqual obtained conf
> +
> +-- | Test a Xen 4.0.1 @xm list --long@ output.
> +case_xen401list :: Assertion
> +case_xen401list = testDomain "xen-xm-list-long-4.0.1.txt" $
> +  Map.fromList
> +    [ ("Domain-0", Domain 0 "Domain-0" 184000.41332 ActualRunning Nothing)
> +    , ("instance1.example.com", Domain 119 "instance1.example.com" 
> 24.116146647
> +      ActualBlocked Nothing)
> +    ]
> +
> +testSuite "Hypervisor/Xen/XmParser"
> +          [ 'prop_config
> +          , 'case_xen401list
> +          ]
> diff --git a/test/hs/htest.hs b/test/hs/htest.hs
> index c2791f3..4358c85 100644
> --- a/test/hs/htest.hs
> +++ b/test/hs/htest.hs
> @@ -51,6 +51,7 @@ import Test.Ganeti.HTools.Loader
>  import Test.Ganeti.HTools.Node
>  import Test.Ganeti.HTools.PeerMap
>  import Test.Ganeti.HTools.Types
> +import Test.Ganeti.Hypervisor.Xen.XmParser
>  import Test.Ganeti.JSON
>  import Test.Ganeti.Jobs
>  import Test.Ganeti.JQueue
> @@ -103,6 +104,7 @@ allTests =
>    , testHTools_Node
>    , testHTools_PeerMap
>    , testHTools_Types
> +  , testHypervisor_Xen_XmParser
>    , testJSON
>    , testJobs
>    , testJQueue

LGTM, thanks.

iustin

Reply via email to