packages feed

shell-escape-0.1.2: test/Echo.hs

{-# LANGUAGE OverloadedStrings
  #-}

import System.Environment
import System.Process (runInteractiveProcess, waitForProcess, ProcessHandle)
import System.IO (Handle, stderr, stdout, stdin, hClose)
import Data.ByteString (ByteString, pack, hGetContents)
import qualified Data.ByteString
import Data.ByteString.Char8 (unpack, hPutStrLn)
import qualified Data.ByteString.Char8 as Char8
import Data.Word
import Control.Applicative
import Control.Monad
import qualified Text.Printf
import Data.IORef
import Data.List

import Test.QuickCheck
import Test.QuickCheck.Monadic

import Text.ShellEscape hiding (sh, bash)


--  It is best to implement the echo test with `printf':
--
-- .  Some echo implementations always interprete backslash escapes like \3
--    and give us no way to turn it off. Dash is like this.
--
-- .  GNU echo can not be made to simply ignore options like `--help'.
--
--  The `printf' implementations available to me -- Bash and Dash --
--  consistently ignore backslash escape sequences and any options that follow
--  the format argument.
--
printf                      ::  (Shell t, Escape t) => t -> IO ByteString
printf escaped               =  do
  (i, o, e, p)              <-  shell escaped cmd
  exit                      <-  waitForProcess p
  hGetContents o <* mapM_ hClose [i, o, e]
 where
  cmd                        =  "printf '%s' " ++ unpack (bytes escaped)


prop_echoBash               ::  ByteString -> Property
prop_echoBash                =  something_prop escapeBash

prop_echoSh                 ::  ByteString -> Property
prop_echoSh                  =  something_prop escapeSh

something_prop esc b         =  monadicIO $ do
  assert =<< run (test printf b (esc b))

escapeSh                    ::  ByteString -> Sh
escapeSh b                   =  escape b

escapeBash                  ::  ByteString -> Bash
escapeBash b                 =  escape b



test cmd original escaped    =  do
  b                         <-  cmd escaped
  Data.ByteString.appendFile "./lengths" (displayLength original)
  Data.ByteString.appendFile "./chars" (Char8.unlines $ displayBytes original)
  when (b /= original) $ do
    err "Result differs from unescaped result:"
    err "Escaped form:"
    err (Char8.unwords . displayBytes $ bytes escaped)
    err "Output:"
    err (Char8.unwords . displayBytes $ b)
    err "Original:"
    err (Char8.unwords . displayBytes $ original)
  return (b == original)


displayBytes                 =  fmap Char8.pack
                             .  fmap (\w ->  Text.Printf.printf "0x%02x" w)
                             .  Data.ByteString.unpack

displayLength                =  Char8.pack
                             .  (\w ->  Text.Printf.printf "%4d\n" w)
                             .  Data.ByteString.length

class (Escape t) => Shell t where
  shell :: t -> String -> IO (Handle, Handle, Handle, ProcessHandle)
instance Shell Sh where
  shell _                    =  sh
instance Shell Bash where
  shell _                    =  bash


sh s = runInteractiveProcess "sh" ["-c", s] Nothing Nothing
bash s = runInteractiveProcess "bash" ["-c", s] Nothing Nothing

instance Arbitrary ByteString where
  arbitrary                  =  do
    bytes                   <-  arbitrary :: Gen [NonZero Word8]
    NonEmpty bytes'         <-  arbitrary :: Gen (NonEmptyList (NonZero Word8))
    pack `fmap` elements
                (fmap unNonZero `fmap` [bytes', bytes', bytes, bytes', bytes'])
   where
    unNonZero (NonZero t) = t

main                         =  do
  runSh                     <-  newIORef True
  runBash                   <-  newIORef True
  testsR                    <-  newIORef 10000
  let testsRwrite            =  writeIORef testsR
      noBash                 =  writeIORef runBash False
      noSh                   =  writeIORef runSh False
  args                      <-  getArgs
  case (sort . nub) args of
    ["--bash", "--sh", d]   ->  testsRwrite (read d)
    ["--bash", "--sh"   ]   ->  return ()
    ["--bash",         d]   ->  noSh >> testsRwrite (read d)
    [          "--sh", d]   ->  noBash >> testsRwrite (read d)
    [                  d]   ->  testsRwrite (read d)
    [                   ]   ->  return ()
    _                       ->  error "Invalid arguments."
  tests                     <-  readIORef testsR
  let msg                    =  "Performing " ++ show tests ++ " tests."
      qcArgs                 =  Args Nothing tests tests 32 False
      qc                     =  quickCheckWith qcArgs
  err "Tests are random ByteStrings, containing any byte but null."
  runSh ?> do
    err "Testing Sh escaping."
    err (Char8.pack msg)
    qc prop_echoSh
  runBash ?> do
    err "Testing Bash escaping."
    err (Char8.pack msg)
    qc prop_echoBash

err                          =  hPutStrLn stderr

(?>)                        ::  IORef Bool -> IO () -> IO ()
ref ?> action                =  readIORef ref >>= (`when` action)