;;; test-ob-haskell-ghci.el --- tests for ob-haskell.el GHCi -*- lexical-binding: t; -*- ;; Copyright (c) 2023-2024 Free Software Foundation, Inc. ;; Authors: Bruno BARBIER ;; This file is part of GNU Emacs. ;; 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 3 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, see . ;;; Commentary: ;; ;;;; Useful references ;; ;; - https://orgmode.org/worg/org-contrib/babel/languages/lang-compat.html ;; - GHCi manual: https://downloads.haskell.org/ghc/latest/docs/users_guide/ghci.html ;;; Code: ;; (require 'org-test "../testing/org-test") (org-test-for-executable "ghci") (unless (featurep 'haskell-mode) (signal 'missing-test-dependency "haskell-mode")) (unless (featurep 'haskell) (signal 'missing-test-dependency "haskell")) ;;; Helpers ;; (defun test-ob-haskell-ghci-checking-buffers (todo) "Check some buffer related invariants.." (when (get-buffer "*haskell*") (error "A buffer named '*haskell*' exists. Can't safely test haskell blocks")) (prog1 (funcall todo) (when-let ((hb (get-buffer "*haskell*"))) ;; We created a "*haskell*" buffer. That shouldn't happen. (error "'ob-haskell' created a buffer named '*haskell*'")))) (defun test-ob-haskell-ghci (args content &optional preamble unprotected) "Execute the code block CONTENT in a new GHCi session; return the result. Add ARGS to the code block argument line. Insert PREAMBLE before the code block. When UNPROTECTED is non-nil, check pre/post conditions." (when (listp content) (setq content (string-join content "\n"))) (unless preamble (setq preamble "")) (let ((todo (lambda () (prog1 (org-test-with-temp-text (concat preamble "\n" "#+begin_src haskell :compile no " args "\n" "" content "\n#+end_src") (org-babel-execute-src-block)))))) (if unprotected (funcall todo) (test-ob-haskell-ghci-checking-buffers todo)))) ;;; Tests ;;;; Hello Worlds. ;; (ert-deftest ob-haskell/hello-world-value-pure () (should (equal "Hello World!" (test-ob-haskell-ghci "" "\"Hello World!\"")))) (ert-deftest ob-haskell/hello-world-value-IO () (should (equal "Hello World!" (test-ob-haskell-ghci "" "return \"Hello World!\"")))) (ert-deftest ob-haskell/hello-world-output () (should (equal "Hello World!" (test-ob-haskell-ghci ":results output" "putStrLn \"Hello World!\"")))) (ert-deftest ob-haskell/hello-world-output-nothing () ;; GHCi prints the value on standard output. So, the last value is part of the output. (should (equal "Hello World!" (test-ob-haskell-ghci ":results output" "return \"Hello World!\"")))) (ert-deftest ob-haskell/hello-world-output-multilines () (should (equal "Hello World!" (test-ob-haskell-ghci ":results output" " :{ main :: IO () main = putStrLn \"Hello World!\" :} main ")))) ;;;; Sessions ;; (ert-deftest ob-haskell/sessions-must-not-share-variables () "Sessions must not share variables." (test-ob-haskell-ghci ":session s1" "x=2" nil) (should (equal 2 (test-ob-haskell-ghci ":session s1" "x" nil))) (test-ob-haskell-ghci ":session s2" "x=3" nil) (should-not (equal 3 (test-ob-haskell-ghci ":session s1" "x" nil))) ) (ert-deftest ob-haskell/session-named-none-means-one-shot-sessions () "When no session, use a new session. \"none\" is a special name that means `no session'." (test-ob-haskell-ghci ":session none" "x=2" nil) (should-not (equal 2 (test-ob-haskell-ghci ":session \"none\"" "x" nil))) (test-ob-haskell-ghci ":session none" "x=2" nil) (should-not (equal 2 (test-ob-haskell-ghci ":session \"none\"" "x" nil)))) (ert-deftest ob-haskell/reuse-variables-in-same-session () "Reuse variables between blocks using the same session." (test-ob-haskell-ghci ":session s1" "x=2" nil) (should (equal 2 (test-ob-haskell-ghci ":session s1" "x")))) (ert-deftest ob-haskell/may-use-the-*haskell*-session () "The user may use the special *haskell* buffer." (when (get-buffer "*haskell*") (error "A buffer named '*haskell*' exists. Can't run this test")) (unwind-protect (progn (test-ob-haskell-ghci ":session *haskell*" "x=2" nil :unprotected) (should (equal 2 (test-ob-haskell-ghci ":session *haskell*" "x" nil :unprotected)))) (with-current-buffer "*haskell*" (let ((kill-buffer-query-functions nil) (kill-buffer-hook nil)) (kill-buffer "*haskell*"))))) ;;;; Values ;; (ert-deftest ob-haskell/value-is-the-last-expression () "Return the value of the last expression." (should (equal 3 (test-ob-haskell-ghci "" '("1" "1+1" "1+1+1")))) (should (equal 3 (test-ob-haskell-ghci "" '("x=1" "y=1+1" "x+y"))))) (ert-deftest ob-haskell/value-is-the-last-expression-2 () "Return the value of the last expression." (should (equal 7 (test-ob-haskell-ghci "" " putStrLn \"a string\" return \"useless\" 3+4 ")))) (ert-deftest ob-haskell/eval-numbers () "Evaluation of numbers." (should (equal 7 (test-ob-haskell-ghci "" "7"))) (should (equal 7.5 (test-ob-haskell-ghci "" "7.5"))) (should (equal 10.0 (test-ob-haskell-ghci "" "10::Double"))) (should (equal 10 (test-ob-haskell-ghci "" "10::Int")))) (ert-deftest ob-haskell/eval-strings () "Evaluation of strings." (should (equal "a string" (test-ob-haskell-ghci "" "\"a string\"")))) ;;;; Output without EOL ;; (ert-deftest ob-haskell/output-without-eol-1 () "Cannot get output from incomplete lines, when entered line by line." :expected-result :failed (should (equal "123" (test-ob-haskell-ghci ":results output" " putStr(\"1\") putStr(\"2\") putStr(\"3\") putStr(\"\\n\") ")))) (ert-deftest ob-haskell/output-without-eol-2 () "Incomplete output lines are OK when using a multiline block." (should (equal "123" (test-ob-haskell-ghci ":results output" " :{ do putStr(\"1\") putStr(\"2\") putStr(\"3\") putStr(\"\\n\") :} ")))) (ert-deftest ob-haskell/output-without-eol-3 () "Incomplete output lines are OK on one line." (should (equal "123" (test-ob-haskell-ghci ":results output" " do { putStr(\"1\"); putStr(\"2\"); putStr(\"3\"); putStr(\"\\n\") } ")))) ;;;; Local variables (ert-deftest ob-haskell/let-one-line () "Local definitions on one line." (should (equal 6 (test-ob-haskell-ghci "" "let { x=2; y=3 } in x*y")))) (ert-deftest ob-haskell/let-multilines-1 () "Local definitions on multiple lines." (should (equal 6 (test-ob-haskell-ghci "" " :{ let { x=2 ; y=3 } in x*y :} ")))) (ert-deftest ob-haskell/let-multilines-2 () "Local definitions on multiple lines, relying on indentation." (should (equal 6 (test-ob-haskell-ghci "" " :{ let x=2 y=3 in x*y :} ")))) ;;;; Declarations with multiple lines. (ert-deftest ob-haskell/decl-multilines-1 () "A multiline declaration, then use it." (should (equal 3 (test-ob-haskell-ghci "" " :{ let length' [] = 0 length' (_:l) = 1 + length' l :} length' [1,2,3] ")))) (ert-deftest ob-haskell/decl-multilines-2 () "A multiline declaration, then use it." (should (equal 5 (test-ob-haskell-ghci "" " :{ length' :: [a] -> Int length' [] = 0 length' (_:l) = 1 + length' l :} length' [1..5] ")))) (ert-deftest ob-haskell/primes () "From haskell.org.""" (should (equal '(2 3 5 7 11 13 17 19 23 29) (test-ob-haskell-ghci "" " :{ primes = filterPrime [2..] where filterPrime (p:xs) = p : filterPrime [x | x <- xs, x `mod` p /= 0] :} take 10 primes ")))) ;;;; Lists ;; (ert-deftest ob-haskell/a-simple-list () "Evaluation of list of values." (should (equal '(1 2 3) (test-ob-haskell-ghci "" "[1,2,3]")))) (ert-deftest ob-haskell/2D-lists () "Evaluation of nested lists into a table." (should (equal '((1 2 3) (4 5 6)) (test-ob-haskell-ghci "" "[[1..3], [4..6]]")))) (ert-deftest ob-haskell/2D-lists-multilines () "Evaluation of nested lists into a table, as multilines." (should (equal '((1 2 3) (4 5 6) (7 8 9)) (test-ob-haskell-ghci "" " :{ [ [1..3] , [4..6] , [7..9] ] :} ")))) ;;;; Tuples ;; (ert-deftest ob-haskell/a-simple-tuple () "Evaluation of tuple of values." (should (equal '(1 2 3) (test-ob-haskell-ghci "" "(1,2,3)")))) (ert-deftest ob-haskell/2D-tuples () "Evaluation of nested tuples into a table." (should (equal '((1 2 3) (4 5 6)) (test-ob-haskell-ghci "" "((1,2,3), (4,5,6))")))) (ert-deftest ob-haskell/2D-tuples-multilines () "Evaluation of nested tuples into a table, as multilines." (should (equal '((1 2 3) (4 5 6) (7 8 9)) (test-ob-haskell-ghci "" " :{ ( (1,2,3) , (4,5,6) , (7,8,9) ) :} ")))) ;;;; Data tables ;; (ert-deftest ob-haskell/int-table-data () "From worg: int-table-data." (should (equal 10 (test-ob-haskell-ghci ":var t=int-table-data" "sum [sum r | r <- t]" "#+name: int-table-data | 1 | 2 | | 3 | 4 |")))) (ert-deftest ob-haskell/float-table-data () "From worg: float-table-data." (should (equal 11.0 (test-ob-haskell-ghci ":var t=float-table-data" "sum [sum r | r <- t]" "#+name: float-table-data | 1.1 | 2.2 | | 3.3 | 4.4 |")))) (ert-deftest ob-haskell/string-table-data () "From worg: string-table-data." (should (equal "abcd" (test-ob-haskell-ghci ":var t=string-table-data" "concat [concat r | r <- t]" "#+name: string-table-data | a | b | | c | d |")))) ;;;; Reuse results ;; (ert-deftest ob-haskell/reuse-table () "Reusing a computed tables." (should (equal 78 (test-ob-haskell-ghci ":var t=a-table" "sum [sum r | r <- t]" "#+name: a-table #+begin_src haskell [ [x..x+2] | x <- [1,4 .. 12] ] #+end_src ")))) ;;;; Not defined errors ;; (ert-deftest ob-haskell/not-defined () "Evaluation of undefined variables." :expected-result :failed (should-error (test-ob-haskell-ghci "" "notDefined :: IO Int"))) (ert-deftest ob-haskell/not-defined-then-defined-1 () "Evaluation of undefined variables. This is a valid haskell source, but, invalid when entered one line at a time in GHCi." :expected-result :failed (should-error (test-ob-haskell-ghci "" " v :: Int v = 4 "))) (ert-deftest ob-haskell/not-defined-then-defined-1-fixed () "Like not-defined-then-defined-1, but using the mutiline marks." (let ((r (test-ob-haskell-ghci "" " :{ v :: Int v = 4 :} "))) (should (eq nil r)))) (ert-deftest ob-haskell/not-defined-then-defined-1-fixed-2 () "Like not-defined-then-defined-1, but using one line." (should (eq nil (test-ob-haskell-ghci "" "v = 4 :: Int")))) (ert-deftest ob-haskell/not-defined-then-defined-2 () "Evaluation of undefined variables, followed by a correct one." ;; ghci output is: ;; | :2:1-4: error: ;; | • Variable not in scope: main :: IO () ;; | • Perhaps you meant ‘min’ (imported from Prelude) ;; | Hello, World! ;; and ob-haskell just reports the last line "Hello, World!". (should (string-match "Variable not in scope" (test-ob-haskell-ghci ":results output" " main :: IO () main = putStrLn \"Hello, World!\" main ")))) ;;;; Imports ;; (ert-deftest ob-haskell/import () "Import and use library." (should (equal 65 (test-ob-haskell-ghci "" " import Data.IORef r <- newIORef 65 readIORef r ")))) (ert-deftest ob-haskell/import-with-vars () "Import and use library with vars." (should (equal 65 (test-ob-haskell-ghci ":var x=65" " import Data.IORef r <- newIORef x readIORef r ")))) ;;;; What is the result? ;; (ert-deftest ob-haskell/results-value-1 () "Don't confuse output and values: nothing." (should (equal nil (test-ob-haskell-ghci ":results value" "return ()")))) (ert-deftest ob-haskell/results-value-2 () "Don't confuse output and values: a list." (should (equal '(1 2) (test-ob-haskell-ghci ":results value" "return [1,2]")))) (ert-deftest ob-haskell/results-value-3 () "Don't confuse output and values: nothing." (should (equal nil (test-ob-haskell-ghci ":results value" "putStrLn \"3\"")))) (ert-deftest ob-haskell/results-value-4 () "Don't confuse output and values: nothing." (should (equal nil (test-ob-haskell-ghci ":results value" " putStrLn \"3\" return () ")))) ;;;; GHCi commands ;; (ert-deftest ob-haskell/ghci-type () "The ghci meta command ':type'." (should (equal "n :: Int" (test-ob-haskell-ghci ":results output" "let n=3::Int\n:type n")))) (ert-deftest ob-haskell/ghci-info () "The ghci meta command ':info' ." (should (string-match-p "repeat :: a -> \\[a\\][ \t]+-- Defined in ‘GHC.List’" (test-ob-haskell-ghci ":results output" ":info repeat")))) (provide 'test-ob-haskell-ghci) ;;; test-ob-haskell-ghci.el ends here