org-mode/testing/lisp/test-ob-haskell-ghci.el

473 lines
13 KiB
EmacsLisp
Raw Normal View History

;;; 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 <brubar.cs@gmail.com>
;; 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 <https://www.gnu.org/licenses/>.
;;; 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" "<point>" 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))))
(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:
;; | <interactive>: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