2026-10-01 20:48:38 +08:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
|
|
|
|
|
module Main (main) where
|
|
|
|
|
|
|
|
|
|
import ChuSQL.CLI.Format (OutputFormat (..), parseFormat, renderCsv, renderJson, renderTable)
|
|
|
|
|
import ChuSQL.CLI.Script (Meta (..), errorHint, parseMeta, statementComplete, stripTerminator, takeStatement)
|
2026-10-02 14:53:02 +08:00
|
|
|
import ChuSQL.Core.Model (Value (..))
|
|
|
|
|
import ChuSQL.Core.Protocol (QueryResult (..), queryResultJson)
|
2026-10-01 20:48:38 +08:00
|
|
|
import Data.Aeson (decode)
|
|
|
|
|
import qualified Data.Aeson as A
|
|
|
|
|
import qualified Data.ByteString.Lazy as BL
|
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import Data.Text.Encoding (encodeUtf8)
|
|
|
|
|
import Test.Hspec
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- chusql-cli 测试:只覆盖本前端的纯逻辑(输出格式、语句缓冲与元命令)。
|
2026-10-01 20:48:38 +08:00
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 测试入口
|
2026-10-01 20:48:38 +08:00
|
|
|
main :: IO ()
|
|
|
|
|
main = hspec spec
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 全部测试用例
|
2026-10-01 20:48:38 +08:00
|
|
|
spec :: Spec
|
|
|
|
|
spec = do
|
|
|
|
|
formatSpec
|
|
|
|
|
scriptSpec
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 输出格式的用例
|
2026-10-01 20:48:38 +08:00
|
|
|
formatSpec :: Spec
|
|
|
|
|
formatSpec = describe "CLI: output formats" $ do
|
|
|
|
|
it "renders a boxed table and marks NULL" $
|
|
|
|
|
renderTable sampleResult
|
|
|
|
|
`shouldBe` T.unlines
|
|
|
|
|
[ "+-----+--------+"
|
|
|
|
|
, "| id | name |"
|
|
|
|
|
, "+-----+--------+"
|
|
|
|
|
, "| 1 | Alice |"
|
|
|
|
|
, "| 2 | NULL |"
|
|
|
|
|
, "+-----+--------+"
|
|
|
|
|
, "(2 rows)"
|
|
|
|
|
]
|
|
|
|
|
it "renders CSV with RFC 4180 quoting and empty NULL fields" $
|
|
|
|
|
renderCsv
|
|
|
|
|
QueryResult
|
|
|
|
|
{ qrColumns = ["id", "name"]
|
|
|
|
|
, qrRows = [[VInt 1, VStr "a,b"], [VNull, VStr "say \"hi\""]]
|
|
|
|
|
, qrRowCount = 2
|
|
|
|
|
, qrTruncated = False
|
|
|
|
|
, qrDatabase = Nothing
|
|
|
|
|
}
|
|
|
|
|
`shouldBe` "id,name\n1,\"a,b\"\n,\"say \"\"hi\"\"\"\n"
|
|
|
|
|
it "renders JSON that parses back to the same result" $
|
|
|
|
|
(decode (BL.fromStrict (encodeUtf8 (renderJson sampleResult))) :: Maybe A.Value)
|
|
|
|
|
`shouldBe` Just (queryResultJson sampleResult)
|
|
|
|
|
it "accepts table/json/csv in any case and rejects anything else" $ do
|
|
|
|
|
parseFormat "TABLE" `shouldBe` Just FormatTable
|
|
|
|
|
parseFormat " json " `shouldBe` Just FormatJson
|
|
|
|
|
parseFormat "csv" `shouldBe` Just FormatCsv
|
|
|
|
|
parseFormat "xml" `shouldBe` Nothing
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 语句缓冲与元命令的用例
|
2026-10-01 20:48:38 +08:00
|
|
|
scriptSpec :: Spec
|
|
|
|
|
scriptSpec = do
|
|
|
|
|
describe "CLI: statement buffering" $ do
|
|
|
|
|
it "a statement is complete only at an unquoted semicolon" $ do
|
|
|
|
|
statementComplete "SELECT 1;" `shouldBe` True
|
|
|
|
|
statementComplete "SELECT 1" `shouldBe` False
|
|
|
|
|
statementComplete "SELECT ';'" `shouldBe` False
|
|
|
|
|
statementComplete "SELECT 'a''b'" `shouldBe` False
|
|
|
|
|
statementComplete "SELECT 1 -- ;" `shouldBe` False
|
|
|
|
|
statementComplete "SELECT /* ; */ 1" `shouldBe` False
|
|
|
|
|
statementComplete "SELECT /* ; */ 1;" `shouldBe` True
|
|
|
|
|
it "splits the first statement off and keeps the rest" $
|
|
|
|
|
takeStatement "SELECT 1; SELECT 2;"
|
|
|
|
|
`shouldBe` Just ("SELECT 1", " SELECT 2;")
|
|
|
|
|
it "drops the terminator before sending SQL to the server" $ do
|
|
|
|
|
stripTerminator "SELECT 1 ; " `shouldBe` "SELECT 1"
|
|
|
|
|
stripTerminator "SELECT 1" `shouldBe` "SELECT 1"
|
|
|
|
|
it "suggests how to look around after a common error" $ do
|
|
|
|
|
errorHint "unknown table: nope" `shouldBe` Just "run \\dt to list the tables of the current database"
|
|
|
|
|
errorHint "unknown column: nope" `shouldBe` Just "run \\d <table> to see the columns of a table"
|
|
|
|
|
errorHint "something else" `shouldBe` Nothing
|
|
|
|
|
describe "CLI: meta commands" $ do
|
|
|
|
|
it "parses the backslash commands" $ do
|
|
|
|
|
parseMeta "\\q" `shouldBe` Just MetaQuit
|
|
|
|
|
parseMeta "\\?" `shouldBe` Just MetaHelp
|
|
|
|
|
parseMeta "\\l" `shouldBe` Just MetaDatabases
|
|
|
|
|
parseMeta "\\dt" `shouldBe` Just MetaTables
|
|
|
|
|
parseMeta "\\d" `shouldBe` Just MetaTables
|
|
|
|
|
parseMeta "\\d users" `shouldBe` Just (MetaDescribe "users")
|
|
|
|
|
parseMeta "\\dr" `shouldBe` Just MetaRoles
|
|
|
|
|
parseMeta "\\roles" `shouldBe` Just MetaRoles
|
|
|
|
|
parseMeta "\\c sales" `shouldBe` Just (MetaConnect "sales")
|
|
|
|
|
parseMeta "\\format CSV" `shouldBe` Just (MetaSetFormat FormatCsv)
|
|
|
|
|
it "leaves ordinary SQL alone" $ do
|
|
|
|
|
parseMeta "SELECT 1" `shouldBe` Nothing
|
|
|
|
|
parseMeta "\\nope" `shouldBe` Just (MetaUnknown "nope")
|
|
|
|
|
parseMeta "\\f xml" `shouldBe` Just (MetaUnknown "format")
|
|
|
|
|
|
|
|
|
|
sampleResult :: QueryResult
|
|
|
|
|
sampleResult =
|
|
|
|
|
QueryResult
|
|
|
|
|
{ qrColumns = ["id", "name"]
|
|
|
|
|
, qrRows = [[VInt 1, VStr "Alice"], [VInt 2, VNull]]
|
|
|
|
|
, qrRowCount = 2
|
|
|
|
|
, qrTruncated = False
|
|
|
|
|
, qrDatabase = Nothing
|
|
|
|
|
}
|