Java, Scala, .NET, Lisp, Python, IDE's, Hibernate, MATLAB, Mathematica, Physics & Other

воскресенье, 27 сентября 2009 г.

Online Judge unit testing - на LISP-е !!

Уже больше недели изучаю язык Lisp. Неоднократно слышал как его хвалили и решил попробовать действительно ли он так хорош. Изучаю книжке Practical Common Lisp. Вот недавно прочитал главу 9. Practical: Building a Unit Test Framework и решил на основе "фреймворка" реализованного в данной главе построить библиотечку для юнит тестирования в Online Judge.

То, что было реализовано в главе 9 книги назвать фреймворком както язык не поворачивается.. Фреймворк в 16 строчек кода (если не учитывать комментарии) - это смешно) Человеку программирующему на java, C#, C++ трудно понять, что там такого супермощного можно закодировать в 16-ти строчках кода.

Итак взяв за основу эти 16 строк кода я прикрутил проверку времени выполнения, используемой памяти, проверку не было ли выброшено исключение во время работы алгоритма. Вообще по возможностям старался приблизится к решению на C# которое описано в посте Online Judge unit testing.

Получился такой код (кто не знаком с лиспом - пропускайте):

; файл unittesting.lisp
(defvar *test-name* nil)
(defvar *test-time-limit* 2)
(defvar *test-memory-limit* 160)
(defvar *data-files-dir* nil)

(define-condition assert-error (error)
  ((text :initarg :text :reader text)))

(defmacro assert-equal (expected real &optional prefix)
  (with-gensyms (nil e r msg prefValue)
  `(let ((,e ,expected) (,r ,real))
     (if (equal ,e ,r)
     t
     (let* ((,msg (format nil "Expected [~a], but was [~a]" ,e ,r))
            (,prefValue ,prefix))      
         (error 'assert-error :text
           (if ,prefValue (concatenate 'string "[" ,prefValue "] " ,msg) ,msg)))
       ))
    ))

(defmacro time-memory-result ((&body body))
  (with-gensyms (nil start res end time memory)
    `(let* ((,start (get-internal-real-time))
             (,res ,body)
             (,end (get-internal-real-time))
             (,time (float (/ (- ,end ,start) internal-time-units-per-second)))
             (,memory (float (/ (sys::%room) (* 1024 1024)))))
       (list ,time ,memory ,res))
    )
  )

(defmacro deftest (name parameters &body body)
  "Define a test function. Within a test function we can call
   other test functions or use 'check' to run individual test
   cases."

  `(defun ,name ,parameters
    (let ((*test-name* (append *test-name* (list ',name))))
      ,@body)))
 
(defun validate (time memory res)
  (let ((violations nil))
    (progn
      (if (> memory *test-memory-limit*)
        (setf violations (cons
         (format nil "[Memory limit exceeded] Expected less than ~aMB, but was ~aMB"
           *test-memory-limit* memory) violations)))
      (if (> time *test-time-limit*)
        (setf violations (cons
         (format nil "[Time limit exceeded] Expected less than ~as, but was ~as"
           *test-time-limit* time) violations)))
      (if (not res)
        (setf violations (cons "Return NIL" violations)))
      violations)
    ))

(defmacro check (&body forms)
  "Run each expression in 'forms' as a test case."
  (with-gensyms (nil time-memory-res time memory res violations ae e)
  `(combine-results
    ,@(loop for f in forms collect        
        `(handler-case
           (let* ((,time-memory-res (time-memory-result ,f))
                   (,time (nth 0 ,time-memory-res))
                   (,memory (nth 1 ,time-memory-res))
                   (,res (nth 2 ,time-memory-res))
                   (,violations (validate ,time ,memory ,res)))
             (report-result ,violations ',f))
           (assert-error (,ae) (report-result (list (text ,ae)) ',f))
           (error (,e) (report-result
              (list (format nil "Error during execution: ~a" ,e)) ',f)))))
    ))
 
(defmacro combine-results (&body forms)
  "Combine the results (as booleans) of evaluating 'forms' in order."
  (with-gensyms (nil allTestsSuccess)
    `(let ((,allTestsSuccess t))
      ,@(loop for f in forms collect `(unless ,f (setf ,allTestsSuccess nil)))
      ,allTestsSuccess)))
 
(defun report-result (violations form)
  "Report the results of a single test case. Called by 'check'."
  (if (= (length violations) 0)
    (format t "pass ... ~a: ~a~%" *test-name* form)
    (format t "FAIL ... ~a: ~a~%   reason: ~{~A~^, ~} ~%"
      *test-name* form violations))
  (= (length violations) 0))


(defun assert-eq-streams (in1 in2)
  (let ((lineNum 0))
    (progn
      (loop
        for line1 = (read-line in1 nil)
        for line2 = (read-line in2 nil)        
        while (or line1 line2)
          do
         (progn
           (incf lineNum)
           (assert-equal line1 line2 (format nil "line ~a" lineNum))))
      t)
    ))

(defun relativep (path)
  (let ((dir (pathname-directory path)))
    (or (= 0 (length dir)) (equal :relative (nth 0 dir)))
    ))

(defun make-absolute-path (path)
  (if (relativep path) (merge-pathnames path *data-files-dir*) path)
  )

(defun file-io-test (algorithm inPath outPath)
  (progn
    (setf inPath (make-absolute-path inPath))
    (setf outPath (make-absolute-path outPath))
    (let ((res (with-open-file (in inPath
                                 :if-does-not-exist nil)
                 (with-output-to-string (out)
                   (funcall algorithm in out))
                 )))
      (with-input-from-string (result res)
        (with-open-file (expected outPath
                          :if-does-not-exist nil)
          (assert-eq-streams expected result)
          ))
      )
    ))



Как этим пользоваться:

(load "F:\\jFiles\\workspaces\\tests\\SPOJ\\ARITH\\ARITH.lisp") ; загружаем файл с алгоритмом
; алгоритм - реализован в функции exec которая принимает 2 потока - входной и выходной.
(load "F:\\jFiles\\workspaces\\tests\\SPOJ\\unittesting.lisp") ; загружаем библиотеку (код выше)

(setf *test-time-limit* 5) ; устанавливаем ограничение по времени в секундах для каждого теста
(setf *test-memory-limit* 16)  ; устанавливаем ограничение по памяти
(setf *data-files-dir* #p"F:\\jFiles\\workspaces\\tests\\SPOJ\\ARITH\\data\\") ; устанавливаем абсолютный
                                                               ; путь к примерам входных и выходных данных

(deftest tests ()  ; определение тестов (каждая строчка внутри check - один тест)
  (check
    (file-io-test #'exec #p"in1.txt" #p"out1.txt")
    (file-io-test #'exec #p"in2.txt" #p"out2.txt")
    (file-io-test #'exec #p"in3.txt" #p"out3.txt")
    (file-io-test #'exec #p"in4.txt" #p"out4.txt")
    (file-io-test #'exec #p"in5.txt" #p"out5.txt")
    (file-io-test #'exec #p"in6.txt" #p"out6.txt")
    ))

(tests) ; запуск тестов


Пример возможного отчета по тестам:

pass ... (TESTS): (FILE-IO-TEST #'EXEC in1.txt out1.txt)
pass ... (TESTS): (FILE-IO-TEST #'EXEC in2.txt out2.txt)
pass ... (TESTS): (FILE-IO-TEST #'EXEC in3.txt out3.txt)
FAIL ... (TESTS): (FILE-IO-TEST #'EXEC in4.txt out4.txt)
reason: [line 18] Expected [ ---------], but was [---------]
FAIL ... (TESTS): (FILE-IO-TEST #'EXEC in5.txt out5.txt)
reason:
Error during execution:
WRITE-LINE: keyword arguments in (#) should occur pairwise
FAIL ... (TESTS): (FILE-IO-TEST #'EXEC in6.txt out6.txt)
reason: [Time limit exceeded] Expected less than 5s, but was 6.0670038s, [Memory limit exceeded] Expected less than 16MB, but was 99.79423MB

Первые 3 теста прошли успешно.
Тест 4 свалился по причине того что в строке 18 вывод алгортма перестал совпадать с ожидаемым выводом.
Тест 5 свалился из-за ошибки в рантайме.
Тест 6 провалился по 2 причинам: превышены лимиты времени и памяти.

Итак, всего в 127 строчках кода удалось выразить все то же что и на C# используя суперфрейморк тестирования и вижуал студию для запуска тестов :) Как по мне - хороший результат)

Комментариев нет:

Отправить комментарий

Постоянные читатели