;;; Gash -- Guile As SHell
;;; Copyright © 2018, 2019, 2021 Timothy Sample <samplet@ngyro.com>
;;;
;;; This file is part of Gash.
;;;
;;; Gash 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.
;;;
;;; Gash 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 Gash.  If not, see <http://www.gnu.org/licenses/>.

(define-module (test-eval)
  #:use-module (gash environment)
  #:use-module (gash eval)
  #:use-module (ice-9 match)
  #:use-module (srfi srfi-64)
  #:use-module (tests unit automake))

;;; Commentary:
;;;
;;; Tests for the eval module.
;;;
;;; Code:

(test-begin "eval")


;;; Basic parameter references.
;;;
;;; FIXME: Test "nounset" ("set -u").

(test-equal "Resolves parameters"
  '("foo")
  (with-variables '(("x" . "foo"))
    (lambda ()
      (eval-word '(<sh-ref> "x")))))

(test-equal "Splits parameter results"
  '("foo" "bar")
  (with-variables '(("x" . "foo bar"))
    (lambda ()
      (eval-word '(<sh-ref> "x")))))

(test-equal "Resolves quoted parameters"
  '("foo")
  (with-variables '(("x" . "foo"))
    (lambda ()
      (eval-word '(<sh-quote> (<sh-ref> "x"))))))

(test-equal "Ignores spaces in quoted parameters"
  '("foo bar")
  (with-variables '(("x" . "foo bar"))
    (lambda ()
      (eval-word '(<sh-quote> (<sh-ref> "x"))))))

(test-equal "Treats empty variables as nothing"
  '()
  (with-variables '(("x" . ""))
    (lambda ()
      (eval-word '(<sh-ref> "x")))))

(test-equal "Treats unset variables as nothing"
  '()
  (with-variables '()
    (lambda ()
      (eval-word '(<sh-ref> "x")))))

(test-equal "Preserves empty variables when quoted"
  '("")
  (with-variables '(("x" . ""))
    (lambda ()
      (eval-word '(<sh-quote> (<sh-ref> "x"))))))

(test-equal "Preserves unset variables when quoted"
  '("")
  (with-variables '()
    (lambda ()
      (eval-word '(<sh-quote> (<sh-ref> "x"))))))


;;; Parameter operations.

;;; or

(test-equal "Handles 'or' when parameter is set"
  '("foo")
  (with-variables '(("x" . "foo"))
    (lambda ()
      (eval-word '(<sh-ref-or> "x" "bar")))))

(test-equal "Handles 'or' when parameter is set and empty"
  '()
  (with-variables '(("x" . ""))
    (lambda ()
      (eval-word '(<sh-ref-or> "x" "bar")))))

(test-equal "Handles 'or' when parameter is unset"
  '("bar")
  (with-variables '()
    (lambda ()
      (eval-word '(<sh-ref-or> "x" "bar")))))

(test-equal "Handles 'or' fall-through without default"
  '()
  (with-variables '()
    (lambda ()
      (eval-word '(<sh-ref-or> "x" #f)))))

;;; or*

(test-equal "Handles 'or*' when parameter is set"
  '("foo")
  (with-variables '(("x" . "foo"))
    (lambda ()
      (eval-word '(<sh-ref-or*> "x" "bar")))))

(test-equal "Handles 'or*' when parameter is set and empty"
  '("bar")
  (with-variables '(("x" . ""))
    (lambda ()
      (eval-word '(<sh-ref-or*> "x" "bar")))))

(test-equal "Handles 'or*' when parameter is unset"
  '("bar")
  (with-variables '()
    (lambda ()
      (eval-word '(<sh-ref-or*> "x" "bar")))))

(test-equal "Handles 'or*' fall-through without default"
  '()
  (with-variables '()
    (lambda ()
      (eval-word '(<sh-ref-or*> "x" #f)))))

;;; or!

(test-equal "Handles 'or!' when parameter is set"
  '(("foo") "foo")
  (with-variables '(("x" . "foo"))
    (lambda ()
      (list (eval-word '(<sh-ref-or!> "x" "bar"))
            (getvar "x")))))

(test-equal "Handles 'or!' when parameter is set and empty"
  '(() "")
  (with-variables '(("x" . ""))
    (lambda ()
      (list (eval-word '(<sh-ref-or!> "x" "bar"))
            (getvar "x")))))

(test-equal "Handles 'or!' when parameter is unset"
  '(("bar") "bar")
  (with-variables '()
    (lambda ()
      (list (eval-word '(<sh-ref-or!> "x" "bar"))
            (getvar "x")))))

(test-equal "Handles 'or!' fall-through without default"
  '(() "")
  (with-variables '()
    (lambda ()
      (list (eval-word '(<sh-ref-or!> "x" #f))
            (getvar "x")))))

;;; or!*

(test-equal "Handles 'or!*' when parameter is set"
  '(("foo") "foo")
  (with-variables '(("x" . "foo"))
    (lambda ()
      (list (eval-word '(<sh-ref-or!*> "x" "bar"))
            (getvar "x")))))

(test-equal "Handles 'or!*' when parameter is set and empty"
  '(("bar") "bar")
  (with-variables '(("x" . ""))
    (lambda ()
      (list (eval-word '(<sh-ref-or!*> "x" "bar"))
            (getvar "x")))))

(test-equal "Handles 'or!*' when parameter is unset"
  '(("bar") "bar")
  (with-variables '()
    (lambda ()
      (list (eval-word '(<sh-ref-or!*> "x" "bar"))
            (getvar "x")))))

(test-equal "Handles 'or!*' fall-through without default"
  '(() "")
  (with-variables '()
    (lambda ()
      (list (eval-word '(<sh-ref-or!*> "x" #f))
            (getvar "x")))))

(test-equal "Does not split fields on assignment"
  '(("foo" "bar") "foo bar")
  (with-variables '(("y" . "foo bar"))
    (lambda ()
      (list (eval-word '(<sh-ref-or!*> "x" (<sh-ref> "y")))
            (getvar "x")))))

;;; FIXME: Test 'assert'.

;;; and

(test-equal "Handles 'and' when parameter is set"
  '("bar")
  (with-variables '(("x" . "foo"))
    (lambda ()
      (eval-word '(<sh-ref-and> "x" "bar")))))

(test-equal "Handles 'and' when parameter is set and empty"
  '("bar")
  (with-variables '(("x" . ""))
    (lambda ()
      (eval-word '(<sh-ref-and> "x" "bar")))))

(test-equal "Handles 'and' when parameter is unset"
  '()
  (with-variables '()
    (lambda ()
      (eval-word '(<sh-ref-and> "x" "bar")))))

(test-equal "Handles 'and' fall-through without default"
  '()
  (with-variables '(("x" . "foo"))
    (lambda ()
      (eval-word '(<sh-ref-and> "x" #f)))))

;;; and*

(test-equal "Handles 'and*' when parameter is set"
  '("bar")
  (with-variables '(("x" . "foo"))
    (lambda ()
      (eval-word '(<sh-ref-and*> "x" "bar")))))

(test-equal "Handles 'and*' when parameter is set and empty"
  '()
  (with-variables '(("x" . ""))
    (lambda ()
      (eval-word '(<sh-ref-and*> "x" "bar")))))

(test-equal "Handles 'and*' when parameter is unset"
  '()
  (with-variables '()
    (lambda ()
      (eval-word '(<sh-ref-and*> "x" "bar")))))

(test-equal "Handles 'and*' fall-through without default"
  '()
  (with-variables '(("x" . "foo"))
    (lambda ()
      (eval-word '(<sh-ref-and*> "x" #f)))))

;;; length

(test-equal "Handles 'length' when parameter is set"
  '("3")
  (with-variables '(("x" . "foo"))
    (lambda ()
      (eval-word '(<sh-ref-length> "x")))))

(test-equal "Handles 'length' when parameter is unset"
  '("0")
  (with-variables '()
    (lambda ()
      (eval-word '(<sh-ref-length> "x")))))


;;; Command substition.

(test-equal "Resolves commands"
  '("foo")
  (eval-word '(<sh-cmd-sub> (<sh-exec> "echo" "foo"))))

(test-equal "Splits command results"
  '("foo" "bar")
  (eval-word '(<sh-cmd-sub> (<sh-exec> "echo" "foo bar"))))

(test-equal "Resolves quoted commands"
  '("foo")
  (eval-word '(<sh-quote> (<sh-cmd-sub> (<sh-exec> "echo" "foo")))))

(test-equal "Ignores spaces in quoted commands"
  '("foo bar")
  (eval-word '(<sh-quote> (<sh-cmd-sub> (<sh-exec> "echo" "foo bar")))))


;;; Arithmetic expansion.

(test-equal "Evaluates arithmetic constant (decimal)"
  '("42")
  (eval-word '(<sh-arithmetic> "42")))

(test-equal "Evaluates arithmetic constant (octal)"
  '("34")
  (eval-word '(<sh-arithmetic> "042")))

(test-equal "Evaluates arithmetic constant (hexadecimal)"
  '("66")
  (eval-word '(<sh-arithmetic> "0x42")))

;;; Binary (and tertiary) operators

(test-equal "Evaluates arithmetic addition"
  '("6")
  (eval-word '(<sh-arithmetic> "2 + 4")))

(test-equal "Evaluates arithmetic subtraction"
  '("2")
  (eval-word '(<sh-arithmetic> "4 - 2")))

(test-equal "Evaluates arithmetic multiplication"
  '("12")
  (eval-word '(<sh-arithmetic> "3 * 4")))

(test-equal "Evaluates arithmetic division"
  '("6")
  (eval-word '(<sh-arithmetic> "19 / 3")))

(test-equal "Evaluates arithmetic modulo"
  '("2")
  (eval-word '(<sh-arithmetic> "32 % 3")))

(test-equal "Evaluates arithmetic left shift"
  '("12")
  (eval-word '(<sh-arithmetic> "3 << 2")))

(test-equal "Evaluates arithmetic right shift"
  '("3")
  (eval-word '(<sh-arithmetic> "15 >> 2")))

(test-equal "Evaluates arithmetic greater than"
  '("1")
  (eval-word '(<sh-arithmetic> "5 > 3")))

(test-equal "Evaluates arithmetic greater than or equal to"
  '("0")
  (eval-word '(<sh-arithmetic> "3 >= 5")))

(test-equal "Evaluates arithmetic less than"
  '("0")
  (eval-word '(<sh-arithmetic> "5 < 3")))

(test-equal "Evaluates arithmetic less than or equal to"
  '("1")
  (eval-word '(<sh-arithmetic> "3 <= 5")))

(test-equal "Evaluates arithmetic equals"
  '("0")
  (eval-word '(<sh-arithmetic> "0 == 1")))

(test-equal "Evaluates arithmetic not equals"
  '("1")
  (eval-word '(<sh-arithmetic> "0 != 1")))

(test-equal "Evaluates arithmetic bitwise and"
  '("4")
  (eval-word '(<sh-arithmetic> "12 & 7")))

(test-equal "Evaluates arithmetic bitwise inclusive or"
  '("9")
  (eval-word '(<sh-arithmetic> "8 | 1")))

(test-equal "Evaluates arithmetic bitwise exclusive or"
  '("5")
  (eval-word '(<sh-arithmetic> "15 ^ 10")))

(test-equal "Evaluates arithmetic logical and"
  '("0")
  (eval-word '(<sh-arithmetic> "0 && 1")))

(test-equal "Evaluates arithmetic logical or"
  '("1")
  (eval-word '(<sh-arithmetic> "0 || 1")))

(test-equal "Evaluates arithmetic conditional"
  '("3")
  (eval-word '(<sh-arithmetic> "0 ? 5 : 3")))

;;; Variables

(test-equal "Evaluates variables in arithmetic"
  '("5")
  (with-variables '(("x" . "3"))
    (lambda ()
      (eval-word `(<sh-arithmetic> "x + 2")))))

(test-equal "Evaluates non-numeric variables as zero in arithmetic"
  '("0")
  (with-variables '(("x" . "hello"))
    (lambda ()
      (eval-word `(<sh-arithmetic> "x")))))

;;; Assignments

(for-each (match-lambda
            ((op . result)
             (test-equal (string-append "Evaluates arithmetic " op)
               result
               (with-variables '(("x" . "7"))
                 (lambda ()
                   (eval-word `(<sh-arithmetic>
                                ,(string-append "x " op " 3")))
                   (getvar "x"))))))
          '(("=" . "3")
            ("*=" . "21")
            ("/=" . "2")
            ("%=" . "1")
            ("+=" . "10")
            ("-=" . "4")
            ("<<=" . "56")
            (">>=" . "0")
            ("&=" . "3")
            ("^=" . "4")
            ("|=" . "7")))

;;; Unary operators

(test-equal "Evaluates arithmetic negation"
  '("-3")
  (with-variables '(("x" . "3"))
    (lambda ()
      (eval-word `(<sh-arithmetic> "-x")))))

(test-equal "Evaluates arithmetic unary plus"
  '("3")
  (with-variables '(("x" . "3"))
    (lambda ()
      (eval-word `(<sh-arithmetic> "+x")))))

(test-equal "Evaluates arithmetic bitwise complement"
  '("-6")
  (eval-word `(<sh-arithmetic> "~5")))

(test-equal "Evaluates arithmetic logical complement"
  '("0")
  (eval-word `(<sh-arithmetic> "!1")))

(test-equal "Evaluates arithmetic negation on the left"
  '("-12")
  (with-variables '(("x" . "3"))
    (lambda ()
      (eval-word `(<sh-arithmetic> "-x * 4")))))

(test-equal "Evaluates arithmetic negation on the right"
  '("0")
  (with-variables '(("x" . "3"))
    (lambda ()
      (eval-word `(<sh-arithmetic> "3 + -x")))))

(test-end "eval")
