473 lines
13 KiB
EmacsLisp
473 lines
13 KiB
EmacsLisp
;;; 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
|