;;; nose.el --- Easy Python test running in Emacs

;; Copyright (C) 2009 Jason Pellerin, Augie Fackler
;; Copyright (C) 2013-2015 Sylvain Benner

;; Created: 04 Apr 2009
;; Version: 0.3
;; Keywords: nose python testing

;; This file is NOT part of GNU Emacs.

;; Licensed under the same terms as Emacs.

;;; Commentary:
;; This gives a bunch of functions that handle running nosetests on a
;; particular buffer or part of a buffer.

;;; Installation

;; In your emacs config:
;;
;; (require 'nose)

;; This version is compatible with Windows.
;; It does not call directly the nosetests script. Instead it calls
;; python with an inline script to call nose.
;; It can launch test suites (require to install the nose fixes via
;; `easy_install nose-fixes`).
;; It is also compatible with virtualenv.

;; By default, the root of a project is found by looking for any of the files
;; '.projectile', 'setup.cfg', '.hg' and '.git'. You can add files to check for
;; to the file list:
;;
;;   (add-to-list 'nose-project-root-files "something")

;; or you can change the project root test to detect in some other way
;; whether a directory is the project root:
;;
;;   (setq nose-project-root-test (lambda (dirname) (equal dirname "foo")))

;; If you want dots as output, rather than the verbose output:
;;
;;   (defvar nose-use-verbose nil) ; default is t

;; Probably also want some keybindings:
;; (add-hook 'python-mode-hook
;;           (lambda ()
;;             (local-set-key "\C-ca" 'nosetests-all)
;;             (local-set-key "\C-cm" 'nosetests-module)
;;             (local-set-key "\C-c." 'nosetests-one)
;;             (local-set-key "\C-cpa" 'nosetests-pdb-all)
;;             (local-set-key "\C-cpm" 'nosetests-pdb-module)
;;             (local-set-key "\C-cp." 'nosetests-pdb-one)))

(require 'cl) ;; for "reduce"

(defvar nose-project-root-files '(".projectile"
                                  "setup.cfg"
                                  ".hg"
                                  ".git"))
(defvar nose-project-root-test 'nose-project-root)
(defvar nose-use-verbose t)

(defun run-nose (&optional tests suite debug failed)
  "run nosetests by calling python instead of nosetests script.
To be able to debug on Windows platform python output must be not buffered.
For more details: http://pswinkels.blogspot.ca/2010/04/debugging-python-code-from-within-emacs.html
"
  (let* ((nose (nosetests-nose-command))
         (where (nose-find-project-root))
         (args (concat (if debug "--pdb" "")
                       " "
                       (if failed "--failed" "")
                       " "
                       (if suite "--test-suite-func=load_tests" "")))
         (tnames (if tests tests "")))
    (if (not where)
        (error
         (format (concat "abort: nosemacs couldn't find a project root, "
                         "looked for any of %S") nose-project-root-files)))
    (funcall (if debug
                 'pdb
               '(lambda (command)
                  (compilation-start command
                                     nil
                                     (lambda (mode) (concat "*nosetests*")))))
             (format
              (concat "%s "
                      (if nose-use-verbose "-v " "")
                      "%s -s -w \"%s\" -c \"%ssetup.cfg\" \"%s\"")
              nose args where where tnames)))
  )

(defun nosetests-nose-command ()
  (let ((nose "python -u -c \"import nose; nose.run()\""))
    (if python-shell-virtualenv-path
        (format "%s/bin/%s" python-shell-virtualenv-path nose)
      nose)))

(defun nosetests-all (&optional debug failed)
  "run all tests"
  (interactive)
  (run-nose nil nil debug failed))

(defun nosetests-failed (&optional debug)
  (interactive)
  (nosetests-all debug t))

(defun nosetests-pdb-all ()
  (interactive)
  (nosetests-all t))

(defun nosetests-module (&optional debug)
  "run nosetests (via eggs/bin/test) on current buffer"
  (interactive)
  (run-nose buffer-file-name nil debug))

(defun nosetests-pdb-module ()
  (interactive)
  (nosetests-module t))

(defun nosetests-suite (&optional debug)
  "run nosetests (via eggs/bin/test) on current suite buffer"
  (interactive)
  (run-nose buffer-file-name t debug))

(defun nosetests-pdb-suite ()
  (interactive)
  (nosetests-suite t))

(defun nosetests-one (&optional debug)
  "run nosetests (via eggs/bin/test) on testable thing
 at point in current buffer"
  (interactive)
  (run-nose (format "%s:%s" buffer-file-name (nose-py-testable)) nil debug))

(defun nosetests-pdb-one ()
  (interactive)
  (nosetests-one t))

(defun nose-py-testable ()
  (let* ((inner-obj (inner-testable))
         (outer (outer-testable))
         ;; elisp can't return multiple values
         (outer-def (car outer))
         (outer-obj (cdr outer)))
    (cond ((equal outer-def "def") outer-obj)
          ((equal inner-obj outer-obj) outer-obj)
          (t (format "%s.%s" outer-obj inner-obj)))))

(defun inner-testable ()
  (save-excursion
    (re-search-backward
     "^ \\{0,4\\}\\(class\\|def\\)[ \t]+\\([a-zA-Z0-9_]+\\)" nil t)
    (buffer-substring-no-properties (match-beginning 2) (match-end 2))))

(defun outer-testable ()
  (save-excursion
    (re-search-backward
     "^\\(class\\|def\\)[ \t]+\\([a-zA-Z0-9_]+\\)" nil t)
    (let ((result
            (buffer-substring-no-properties (match-beginning 2) (match-end 2))))

      (cons
       (buffer-substring-no-properties (match-beginning 1) (match-end 1))
       result))))

(defun nose-find-project-root (&optional dirname)
  (let ((dn
         (if dirname
             dirname
           (file-name-directory buffer-file-name))))
    (cond ((funcall nose-project-root-test dn) (expand-file-name dn))
          ((equal (expand-file-name dn) "/") nil)
        (t (nose-find-project-root
             (file-name-directory (directory-file-name dn)))))))

(defun nose-project-root (dirname)
  (reduce '(lambda (x y) (or x y))
          (mapcar (lambda (d) (member d (directory-files dirname)))
                  nose-project-root-files)))

(provide 'nose)

;;; nose.el ends here
