packages feed

nirum-0.5.0: test/Nirum/Targets/Python/CodeGenSpec.hs

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE QuasiQuotes #-}
module Nirum.Targets.Python.CodeGenSpec
    ( makeDummySource
    , makeDummySource'
    , pythonVersionSpecs
    , spec
    ) where

import Control.Monad
import Data.Either
import Data.Maybe

import Data.SemVer hiding (Identifier, metadata)
import Test.Hspec.Meta
import Text.Email.Validate (emailAddress)
import Text.InterpolatedString.Perl6 (q, qq)

import Nirum.Constructs.Annotation (empty)
import Nirum.Constructs.Identifier
import Nirum.Constructs.Module
import Nirum.Constructs.ModulePath
import Nirum.Constructs.TypeDeclaration
import Nirum.Package.Metadata
import Nirum.Package.ModuleSet
import Nirum.PackageSpec (createPackage)
import Nirum.Targets.Python (Source (..))
import Nirum.Targets.Python.CodeGen hiding (empty, renames)
import qualified Nirum.Targets.Python.CodeGen as CodeGen
import Nirum.TypeInstance.BoundModule

makeDummySource' :: [Identifier] -> Module -> RenameMap -> Source
makeDummySource' pathPrefix m renames =
    Source pkg $ fromJust $ resolveBoundModule ["foo"] pkg
  where
    mp :: [Identifier] -> ModulePath
    mp identifiers = fromJust $ fromIdentifiers (pathPrefix ++ identifiers)
    metadata' :: Metadata Python
    metadata' = Metadata
        { Nirum.Package.Metadata.version = Data.SemVer.version 1 2 3 [] []
        , authors =
              [ Author
                    { name = "John Doe"
                    , email = Just (fromJust $ emailAddress "john@example.com")
                    , uri = Nothing
                    }
              ]
        , description = Just "Package description"
        , license = Just "MIT"
        , Nirum.Package.Metadata.keywords = ["sample", "example", "nirum"]
        , target = Python "sample-package" minimumRuntime renames ["classifier"]
        }
    pkg :: Package Python
    pkg = createPackage
            metadata'
            [ (mp ["foo"], m)
            , ( mp ["foo", "bar"]
              , Module [ Import (mp ["qux"]) "path" "path" empty
                       , TypeDeclaration "path-unbox" (UnboxedType "path") empty
                       , TypeDeclaration "int-unbox"
                                         (UnboxedType "bigint") empty
                       , TypeDeclaration "point"
                                         (RecordType [ Field "x" "int64" empty
                                                     , Field "y" "int64" empty
                                                     ])
                                         empty
                       ] Nothing
              )
            , ( mp ["qux"]
              , Module
                  [ TypeDeclaration "path" (Alias "text") empty
                  , TypeDeclaration "name" (UnboxedType "text") empty
                  ]
                  Nothing
              )
            ]
            []

makeDummySource :: Module -> Source
makeDummySource m = makeDummySource' [] m []

pythonVersionSpecs :: (PythonVersion -> Spec) -> Spec
pythonVersionSpecs =
    parallel . forM_ ([Python2, Python3] :: [PythonVersion])

spec' :: Spec
spec' = pythonVersionSpecs $ \ ver -> do
    let empty' = CodeGen.empty ver
        -- compileError :: CodeGen a -> Maybe CompileError
        compileError cg = either Just (const Nothing) $ fst $
            runCodeGen cg empty'

    describe [qq|CodeGen ($ver)|] $ do
        specify "packages and imports" $ do
            let c = do
                    insertStandardImport "sys"
                    insertThirdPartyImports
                        [("nirum", ["serialize_unboxed_type"])]
                    insertLocalImport ".." "Gender"
                    insertStandardImport "os"
                    insertThirdPartyImports [("nirum", ["serialize_enum_type"])]
                    insertThirdPartyImportsA
                        [("nirum.constructs", [("name_dict_type", "NameDict")])]
                    insertLocalImport ".." "Path"
            let (e, ctx) = runCodeGen c empty'
            e `shouldSatisfy` isRight
            standardImports ctx `shouldBe` [("os", "os"), ("sys", "sys")]
            thirdPartyImports ctx `shouldBe`
                [ ( "nirum"
                  , [ ("serialize_unboxed_type", "serialize_unboxed_type")
                    , ("serialize_enum_type", "serialize_enum_type")
                    ]
                  )
                , ( "nirum.constructs"
                  , [("name_dict_type", "NameDict")]
                  )
                ]
            localImports ctx `shouldBe` [("..", ["Gender", "Path"])]
            localImportsMap ctx `shouldBe`
                [ ( ".."
                  , [ ("Gender", "Gender")
                    , ("Path", "Path")
                    ]
                  )
                ]
        specify "importStandardLibrary" $ do
            let codeGen1 = importStandardLibrary "io"
            let (e1, ctx1) = runCodeGen codeGen1 empty'
            e1 `shouldBe` Right "_io"
            standardImports ctx1 `shouldBe` [("_io", "io")]
            standardImportSet ctx1 `shouldBe` ["io"]
            thirdPartyImports ctx1 `shouldBe` []
            localImports ctx1 `shouldBe` []
            compileError codeGen1 `shouldBe` Nothing
            -- importing a nested module, i.e., import path that contains "."
            let codeGen2 = codeGen1 >> importStandardLibrary "os.path"
            let (e2, ctx2) = runCodeGen codeGen2 empty'
            e2 `shouldBe` Right "_os_path"
            standardImports ctx2 `shouldBe`
                [("_os_path", "os.path"), ("_io", "io")]
            standardImportSet ctx2 `shouldBe` ["os.path", "io"]
            thirdPartyImports ctx2 `shouldBe` []
            localImports ctx2 `shouldBe` []
            compileError codeGen2 `shouldBe` Nothing
            -- importing a module that begins with "_"
            let codeGen3 = codeGen2 >> importStandardLibrary "__builtin__"
            let (e3, ctx3) = runCodeGen codeGen3 empty'
            e3 `shouldBe` Right "__builtin__"
            standardImports ctx3 `shouldBe`
                [ ("__builtin__", "__builtin__")
                , ("_os_path", "os.path")
                , ("_io", "io")
                ]
            standardImportSet ctx3 `shouldBe` ["__builtin__", "os.path", "io"]
            thirdPartyImports ctx3 `shouldBe` []
            localImports ctx3 `shouldBe` []
            compileError codeGen3 `shouldBe` Nothing
        specify "importBuiltins" $ do
            let codeGen1 = importBuiltins
            let (e1, ctx1) = runCodeGen codeGen1 empty'
            e1 `shouldSatisfy` isRight
            standardImports ctx1 `shouldBe`
                [ if ver == Python2
                  then ("__builtin__", "__builtin__")
                  else ("__builtin__", "builtins")
                ]
            standardImportSet ctx1 `shouldBe`
                [if ver == Python2 then "__builtin__" else "builtins"]
            thirdPartyImports ctx1 `shouldBe` []
            localImports ctx1 `shouldBe` []
            compileError codeGen1 `shouldBe` Nothing
        specify "insertStandardImport" $ do
            let codeGen1 = insertStandardImport "sys"
            let (e1, ctx1) = runCodeGen codeGen1 empty'
            e1 `shouldSatisfy` isRight
            standardImports ctx1 `shouldBe` [("sys", "sys")]
            standardImportSet ctx1 `shouldBe` ["sys"]
            thirdPartyImports ctx1 `shouldBe` []
            localImports ctx1 `shouldBe` []
            compileError codeGen1 `shouldBe` Nothing
            let codeGen2 = codeGen1 >> insertStandardImport "os"
            let (e2, ctx2) = runCodeGen codeGen2 empty'
            e2 `shouldSatisfy` isRight
            standardImports ctx2 `shouldBe` [("os", "os"), ("sys", "sys")]
            standardImportSet ctx2 `shouldBe` ["os", "sys"]
            thirdPartyImports ctx2 `shouldBe` []
            localImports ctx2 `shouldBe` []
            compileError codeGen2 `shouldBe` Nothing
        specify "insertStandardImportA" $ do
            let codeGen1 = insertStandardImportA "_csv" "csv"
            let (e1, ctx1) = runCodeGen codeGen1 empty'
            e1 `shouldSatisfy` isRight
            standardImports ctx1 `shouldBe` [("_csv", "csv")]
            standardImportSet ctx1 `shouldBe` ["csv"]
            thirdPartyImports ctx1 `shouldBe` []
            localImports ctx1 `shouldBe` []
            compileError codeGen1 `shouldBe` Nothing
            let codeGen2 = codeGen1 >> insertStandardImportA "_gc" "gc"
            let (e2, ctx2) = runCodeGen codeGen2 empty'
            e2 `shouldSatisfy` isRight
            standardImports ctx2 `shouldBe` [("_gc", "gc"), ("_csv", "csv")]
            standardImportSet ctx2 `shouldBe` ["gc", "csv"]
            thirdPartyImports ctx2 `shouldBe` []
            localImports ctx2 `shouldBe` []
            compileError codeGen2 `shouldBe` Nothing
        specify "collectionsAbc" $ do
            let expected@(expectedModule, _) = case ver of
                    Python2 -> ("_collections", "collections")
                    Python3 -> ("_collections_abc", "collections.abc")
            let (abc, ctx) = runCodeGen collectionsAbc empty'
            abc `shouldBe` Right expectedModule
            standardImports ctx `shouldBe` [expected]
        specify "baseStringClass" $ do
            let (baseString, ctx) = runCodeGen baseStringClass empty'
            case ver of
                Python2 -> do
                    baseString `shouldBe` Right "__builtin__.basestring"
                    standardImports ctx `shouldBe`
                        [("__builtin__", "__builtin__")]
                Python3 -> do
                    baseString `shouldBe` Right "__builtin__.str"
                    standardImports ctx `shouldBe` [("__builtin__", "builtins")]
        specify "baseIntegerClass" $ do
            let (baseString, ctx) = runCodeGen baseIntegerClass empty'
            case ver of
                Python2 -> do
                    baseString `shouldBe`
                        Right "(__builtin__.int, __builtin__.long)"
                    standardImports ctx `shouldBe`
                        [("__builtin__", "__builtin__")]
                Python3 -> do
                    baseString `shouldBe` Right "__builtin__.int"
                    standardImports ctx `shouldBe` [("__builtin__", "builtins")]

        let evalContext f = snd $ runCodeGen f empty'
        let req = empty'
        let req2 = req { dependencies = ["six"] }
        let req3 = req { optionalDependencies = [((3, 4), ["enum34"])] }
        specify "addDependency" $ do
            evalContext (addDependency "six") `shouldBe` req2
            evalContext (addDependency "six" >> addDependency "six")
                `shouldBe` req2
            evalContext (addDependency "nirum") `shouldBe`
                req { dependencies = ["nirum"] }
            evalContext (addDependency "six" >> addDependency "nirum")
                `shouldBe` req2 { dependencies = ["nirum", "six"] }
        specify "addOptionalDependency" $ do
            evalContext (addOptionalDependency (3, 4) "enum34") `shouldBe` req3
            evalContext ( addOptionalDependency (3, 4) "enum34"
                        >> addOptionalDependency (3, 4) "enum34"
                        )
                `shouldBe` req3
            evalContext (addOptionalDependency (3, 4) "ipaddress") `shouldBe`
                req { optionalDependencies = [((3, 4), ["ipaddress"])] }
            evalContext ( addOptionalDependency (3, 4) "enum34"
                        >> addOptionalDependency (3, 4) "ipaddress"
                        )
                `shouldBe` req
                    { optionalDependencies = [ ((3, 4), ["enum34", "ipaddress"])
                                             ]
                    }
            evalContext (addOptionalDependency (3, 5) "typing")
                `shouldBe` req { optionalDependencies = [((3, 5), ["typing"])] }
            evalContext ( addOptionalDependency (3, 4) "enum34"
                        >> addOptionalDependency (3, 5) "typing"
                        )
                `shouldBe` req3
                    { optionalDependencies = [ ((3, 4), ["enum34"])
                                             , ((3, 5), ["typing"])
                                             ]
                    }

spec :: Spec
spec = do
    spec'

    describe "toClassName" $ do
        it "transform the facial name of the argument into PascalCase" $ do
            toClassName "test" `shouldBe` "Test"
            toClassName "hello-world" `shouldBe` "HelloWorld"
        it "appends an underscore to the result if it's a reserved keyword" $ do
            toClassName "true" `shouldBe` "True_"
            toClassName "false" `shouldBe` "False_"
            toClassName "none" `shouldBe` "None_"

    describe "toAttributeName" $ do
        it "transform the facial name of the argument into snake_case" $ do
            toAttributeName "test" `shouldBe` "test"
            toAttributeName "hello-world" `shouldBe` "hello_world"
        it "appends an underscore to the result if it's a reserved keyword" $ do
            toAttributeName "def" `shouldBe` "def_"
            toAttributeName "lambda" `shouldBe` "lambda_"
            toAttributeName "nonlocal" `shouldBe` "nonlocal_"

    specify "toImportPath" $ do
        let (Source pkg _) = makeDummySource $ Module [] Nothing
            target' = target $ metadata pkg
        toImportPath target' ["foo", "bar"] `shouldBe` "foo.bar"

    describe "toImportPaths" $ do
        it "adds ancestors of packages" $ do
            let (Source pkg _) = makeDummySource $ Module [] Nothing
                modulePaths = keysSet $ modules pkg
                target' = target $ metadata pkg
            toImportPaths target' modulePaths `shouldBe`
                [ "foo"
                , "foo.bar"
                , "qux"
                ]
        it "applies renames before add ancestors of packages" $ do
            let (Source pkg _) = makeDummySource' [] (Module [] Nothing)
                                                  [(["foo"], ["f", "oo"])]
                modulePaths = keysSet $ modules pkg
                target' = target $ metadata pkg
            toImportPaths target' modulePaths `shouldBe`
                [ "f"  -- > "f" should be added
                , "f.oo"
                , "f.oo.bar"
                , "qux"
                ]

    specify "renameModulePath" $ do
        let renames = [ (["foo"], ["poo"])
                      , (["foo", "bar"], ["foo"])
                      , (["baz"], ["p", "az"])
                      ] :: RenameMap
        renameModulePath renames ["foo"] `shouldBe` ["poo"]
        renameModulePath renames ["foo", "baz"] `shouldBe` ["poo", "baz"]
        renameModulePath renames ["foo", "bar"] `shouldBe` ["foo"]
        renameModulePath renames ["foo", "bar", "qux"] `shouldBe` ["foo", "qux"]
        renameModulePath renames ["baz"] `shouldBe` ["p", "az"]
        renameModulePath renames ["baz", "qux"] `shouldBe` ["p", "az", "qux"]
        renameModulePath renames ["qux", "foo"] `shouldBe` ["qux", "foo"]

    specify "indent" $ do
        indent "    " ("a\n    b\n\nc\n" :: Code) `shouldBe`
            "    a\n        b\n\n    c\n"
        indent "    " ("\"\"\"foo\nbar\n\"\"\"" :: Code) `shouldBe`
            "    \"\"\"foo\n    bar\n    \"\"\""

    specify "stringLiteral" $ do
        stringLiteral "asdf" `shouldBe` [q|"asdf"|]
        stringLiteral [q|Say 'hello world'|]
            `shouldBe` [q|"Say 'hello world'"|]
        stringLiteral [q|Say "hello world"|]
            `shouldBe` [q|"Say \"hello world\""|]
        stringLiteral "Say '\xc548\xb155'"
            `shouldBe` [q|u"Say '\uc548\ub155'"|]