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

суббота, 24 октября 2009 г.

Программа для резервного копирования / синхронизации данных.

Написал на Steel Bank Common Lisp.

Алгоритм простой:
  • Если файл/директория есть в src но нет в dest - то он/она копируется в dest.
  • Если файл/директория есть в dest но нет в src - то он/она удаляется из dest.
  • Если файл есть в src и в dest, но размеры их не совпадают - то файл из src переписывает файл из dest.
  • Алгоритм вызывается рекурсивно для всех поддиректорий.
Если не удалось скопировать файл/создать директорию - то выводится сообщение об ошибке и алгоритм продолжает работать дальше.

Работает, я считаю, довольно шустро - у меня синхронизация папки 1.4 гб с 53 тыс файлов занимает примерно 10 минут.


;;;; Created on 2009-10-18 13:12:10

(load "F:/Lisp/cl-fad-0.6.3/load.lisp")

(defmacro my-with-gensyms ((&rest names) &body body)
  `(let ,(loop for n in names collect `(,n (gensym)))
     ,@body))

(defmacro exec-time (executions (&body body))
  (my-with-gensyms (start end time)
    `(progn
       (setf ,start (get-internal-real-time))
       (loop for i from 1 to ,executions do ,body)
       (setf ,end (get-internal-real-time))
       (setf ,time (float (/ (- ,end ,start) internal-time-units-per-second)))
       ,time)
    )
  )

(defun path-to-str (path)
  (format nil "~a" path))

(defun file-len (filepath)
  (with-open-file (in filepath) (file-length in)))

(defun dirs-and-files (path)
  (let* ((files-dirs (cl-fad:list-directory path))
         (dirs (remove-if-not #'cl-fad:directory-pathname-p files-dirs))
         (files (remove-if #'cl-fad:directory-pathname-p files-dirs)))    
    (values dirs files)))

(defun file-name (path)
  (let ((ext (pathname-type path)))
    (concatenate 'string (pathname-name path)
                 (if ext (concatenate 'string "." ext)))
    ))

(defun dir-name (path)
  (car (last (pathname-directory path))))

(defun new-deleted-existed (dest-paths src-paths is-files)
  (let* ((key-func (if is-files #'file-name #'dir-name))
         (new (set-difference src-paths dest-paths
                              :key key-func :test #'string-equal))
         (deleted (set-difference dest-paths src-paths
                              :key key-func :test #'string-equal))
         (existed (intersection src-paths dest-paths
                              :key key-func :test #'string-equal)))
    (values new deleted existed)))

(defun get-relative (base-path full-path)
  (let ((base-path-str (path-to-str base-path))
        (full-path-str (path-to-str full-path)))
    (pathname (subseq full-path-str (length base-path-str)))
    ))

(defun full-dest-path (base-dest-path base-src-path full-src-path)
  (let ((relative (get-relative base-src-path full-src-path)))
    (merge-pathnames relative base-dest-path)
    ))

(defmacro sefe-progn (&body body)
  `(handler-case (progn ,@body) (error (r) (format t "~a~%" r))))

(defun synchronize (dest src)  
  (let ((dest (pathname dest)) (src (pathname src))
        (dirs-src nil) (files-src nil) (dirs-dest nil) (files-dest nil)
        (new-files nil) (deleted-files nil) (existed-files nil)
        (new-dirs nil) (deleted-dirs nil) (existed-dirs nil))
    (setf (values dirs-src files-src) (dirs-and-files src))
    (setf (values dirs-dest files-dest) (dirs-and-files dest))
    (setf (values new-files deleted-files existed-files)
          (new-deleted-existed files-dest files-src T))
    (setf (values new-dirs deleted-dirs existed-dirs)
          (new-deleted-existed dirs-dest dirs-src nil))    
    (loop for new in new-files
          for new-dest = (full-dest-path dest src new)
          do (sefe-progn
               (format t "Copy FROM ~a TO ~a~%" new new-dest)
               (cl-fad:copy-file new new-dest)))  
    (loop for deleted in deleted-files
          do (sefe-progn
               (format t "Delete file ~a~%" deleted)
               (delete-file deleted)))
    (loop for existed in existed-files
          for existed-dest = (full-dest-path dest src existed)
          do (sefe-progn
              (if (/= (file-len existed) (file-len existed-dest))
                 (progn
                   (format t "Overwrite FROM ~a TO ~a~%" existed existed-dest)
                   (cl-fad:copy-file existed existed-dest :overwrite t)))))
    (loop for new in new-dirs
          for new-dest = (full-dest-path dest src new)  
          do (sefe-progn
               (format t "Create directory ~a~%" new-dest)
               (ensure-directories-exist new-dest)
               (synchronize new-dest new)))
    (loop for deleted in deleted-dirs
          do (sefe-progn
               (format t "Delete directory ~a~%" deleted)
               (cl-fad:delete-directory-and-files deleted)))
    (loop for existed in existed-dirs
          for existed-dest = (full-dest-path dest src existed)
          do (synchronize existed-dest existed))
    ))

(defun main (args)
  (let ((src (if (= 3 (length args))
              (nth 1 args) "F:/jFiles/workspaces/tests/file-sync/2"))
        (dest (if (= 3 (length args))
              (nth 2 args) "F:/jFiles/workspaces/tests/file-sync/1")))
    (cl-fad:pathname-as-directory "F:/jFiles/workspaces/tests/file-sync/2")
    (format t "Synchronize ~a ==> ~a~%" src dest)
    (let ((time
          (exec-time 1 (synchronize (cl-fad:pathname-as-directory dest)
                                    (cl-fad:pathname-as-directory src)))))
      (format t "Finished [execution time: ~as]" time))
    ))
     
(main *posix-argv*)
 


Теперь небольшая инструкция, как сделать так, чтобы программа заработала.

Для запуска вам понадобиться SBCL и библиотечка cl-fad которую я использовал для упрощения работы с файлами. В программе указан абсолютный путь к библиотеке "F:/Lisp/cl-fad-0.6.3/load.lisp" - необходимо будет заменить его на путь куда вы установите библиотеку. Сохранив код в файл, например sync.lisp, его можно запускать батником, например таким образом: call sbcl --script sync.lisp "F:/jFiles/" "O:/jFiles/". Возможна одна проблема - при запуске программа будет ругаться что библиотека cl-fad якобы откомпилирована более старой версией SBCL. Не пугайтесь - решается просто, надо пойти в директорию установки cl-fad и удалить файл packages.fasl. Тогда при повторном запуске cl-fad будет перекомпилирована.


Осталось добавить батник в планировщик заданий и радоваться жизни)

воскресенье, 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# используя суперфрейморк тестирования и вижуал студию для запуска тестов :) Как по мне - хороший результат)

четверг, 17 сентября 2009 г.

Для тех кто работает в связке Eclipse, TortoiseSVN будет полезно

Часто приходится выполнять действие blame для некоторого файла с исходным кодом.

Что для этого нужно? Я в свое время нашел для себя такую последовательность действий:
Итак: исходная точка - файл открыт в редакторе эклипса.
  1. Alt + Enter открываем свойства файла.
  2. Из Location: копируем путь к папке в которой находится файл
  3. Win+E открываем окно виндовс эксплорера
  4. Ctrl+V вставляем путь
  5. Enter - открываем папку
  6. Ищем глазами файл.. (слишком долго..)
  7. Нашли файл -> правой кнопкой -> TortoiseSVN -> Blame (долго)
Аж 7 шагов!!! Слишком долго как для такого простого действия.

Я решил ускорить этот процесс:
  1. Alt + Enter открываем свойства файла.
  2. Из Location: копируем путь к файлу
  3. Win+D чтобы перейти к рабочему столу
  4. клик по blame.bat
Это уже лучше!

Для того чтобы все это работало вам необходимо:
1) Скачать пакет утилит outwit http://www.dmst.aueb.gr/dds/sw/outwit/
Нас там будет интересовать winclip, которая может вытаскивать текст из буфера обмена.
2) Указать в переменной Path путь к outwit-bin-1.25\bin
3) Также в Path должен быть путь к \TortoiseSVN\bin
4) скрипт blame.bat (не пугайтесь; обьяснять не буду; если интересно - сами попробуйте понять):
<nul (set/p z=.exec_blame.bat ) > .exec_buffer.bat
winclip -p >> .exec_buffer.bat
call .exec_buffer.bat
5) в той же директории где находится blame.bat создайте вспомогательный .exec_blame.bat. Его содержание:
TortoiseProc /command:blame /path:%1

Это все)

суббота, 29 августа 2009 г.

Что делает функцию разделения строки split в джаве такой "уникальной"

Казалось бы, функция split разделения строки на части по заданному разделителю, что в ней может быть такого особенного. Ну делит и делит себе, все отлично..

Посмотрим на пример написанный на Python:

s = ',1,,2,,'
tokens = s.split(',')
print('start')
for token in tokens:
print(token)
print('end')
_Winnie Colorizer


Выполняем.. вывод:
start

1

2


end

Все четко и предсказуемо. На стыке разделителей считается что находится пустая строка.

Теперь посмотрим пример на C#:

String[] tokens = ",1,,2,,".Split(new []{','});
Console.WriteLine("start");
for (int i = 0; i < tokens.Length; ++i )
{
Console.WriteLine(tokens[i]);
}
Console.WriteLine("end");
_Winnie Colorizer


Вывод аналогичен:
start

1

2


end

Ок, значит если я сейчас напишу код на java

String[] tokens = ",1,,2,,".split(",");
System.out.println("start");
for (int i = 0; i < tokens.length; ++i)
{
System.out.println(tokens[i]);
}
System.out.println("end");
_Winnie Colorizer


то можно с полной уверенность ожидать что вывод будет точно таким же. Ага.. Как же - черта с два!

start

1

2
end

Вот таким образом. Стоящие подряд разделители в конце строки почему-то полностью игнорируются.
Джавадок говорит следующее:
This method works as if by invoking the two-argument split method with the given expression and a limit argument of zero. Trailing empty strings are therefore not included in the resulting array.

Опять не понятно - с чего вдруг "empty strings are therefore not included in the resulting array". Ситуацию проясняет только джавадок к методу:
public String[] split(String regex, int limit)

...
The limit parameter controls the number of times the pattern is applied and therefore affects the length of the resulting array. If the limit n is greater than zero then the pattern will be applied at most n - 1 times, the array's length will be no greater than n, and the array's last entry will contain all input beyond the last matched delimiter. If n is non-positive then the pattern will be applied as many times as possible and the array can have any length. If n is zero then the pattern will be applied as many times as possible, the array can have any length, and trailing empty strings will be discarded.
...

Как жаль. А мне было сначала показалось что я нашел багу.. Оказалось просто такая вот дурацкая особенность, которую надо иметь в виду.

понедельник, 24 августа 2009 г.

Коварный Hibernate или проблемма @OneToOne(targetEntity = SomeAbstractEntityClass, fetch = FetchType.LAZY)

Как известно хибернет умеет загружать сущности по асоциации OneToOne ленивым образом.
Для этого нужно указать в аннотации @OneToOne что fetch = FetchType.LAZY. Что это дает?
А то что очень часто когда мы достаем некоторую сущность из базы, нам не нужны сущности прилинкованные к ней. Тогда и вытаскивать их из базы нет смысла. Вот как раз FetchType.LAZY дает то, что прилинкованная по OneToOne сущность подгружается только тогда когда мы начинаем обращаться к полям этой сущности. Полезная штука.. НО! Совсем недавно натолкнулся на совершенно невероятную на первый взгляд вещь..

Покажу на примере.
Пусть у нас имеются такие сущности:

AbstractWeapon implements IWeapon - абстрактное оружие.
Sword extends AbstractWeapon implements ISword - меч.
Bow extends AbstractWeapon implements IBow - лук.
Интерфейсы ISward и IBow наследуются от IWeapon.

Warrior - воин. Который владеет неким "абстрактным" оружием:
@OneToOne(targetEntity = AbstractWeapon.class, fetch = FetchType.LAZY)
@JoinColumn(name = "wr_wp_aa")
public IWeapon getWeapon()
.....

И предположим что у воина есть метод, который по типу оружия определяет к какому классу отнести воина.
Если это меч то - мечник, если лук то соответственно - лучник.

   public WarriorType getType()
{
WarriorType type;

if (weapon instanceof ISword)
{
type = WarriorType.SWORDSMAN;
}
else if (weapon instanceof IBow)
{
type = WarriorType.ARCHER;
}
else
{
throw new IllegalStateException("incorrect type of weapon: " + weapon);
}

return type;
}
_Winnie Colorizer


Теперь сохраняем в базу данных одно оружие - меч, и одного воина - который владеет этоим мечем. А теперь внимание вопрос: что будет если сейчас вытащить этого воина средствами хибернета, и попробовать узнать его тип, вызвав метод getType?

Вы было подумали что метод вернет WarriorType.SWORDSMAN? Увы, вынужден вас огорчить, но вы жестоко ошиблись.. Метод кинет исключение IllegalStateException.

Начинаем дебажить и выясняем всю правду:
weapon на самом деле не Sword, а обьект некого таинственного типа AbstractWeapon_$_javassist_1. Смотрим внимательнее, и видим что дерево наследования у AbstractWeapon_$_javassist_1 имеет примерно такой вид:

com.greentea.relaxation.serverplays.shp.business.weapons.AbstractWeapon_$$_javassist_1
extends
com.greentea.relaxation.serverplays.shp.business.weapons.AbstractWeapon
extends
com.greentea.relaxation.serverplays.shp.business.AbstractEntity
implements
com.greentea.relaxation.serverplays.shp.business.IEntity
implements
com.greentea.relaxation.serverplays.shp.business.weapons.IWeapon
extends
com.greentea.relaxation.serverplays.shp.business.IEntity
implements
org.hibernate.proxy.HibernateProxy
extends
java.io.Serializable
javassist.util.proxy.ProxyObject

Нигде! Как вы видите - нигде, нету в этом дереве типа Sword. Итак, выясняется что weapon это прокси
(реализует интерфейсы org.hibernate.proxy.HibernateProxy и javassist.util.proxy.ProxyObject) для
класса AbstractWeapon. И никаким образом в рантайме честно узнать подтип по instanceof или привести к одному из типов производных от AbstractWeapon не получится..

Существуют конечно обходные пути, например:
- Метод Hibernate.getClass - вернет тип Sword.
- Сам пример немножко надуманный, и можно было бы реализовать getType по другому.
- Также можно вообще радикально решить проблемму отказавшись от LAZY.

Мне кажется что Hibernate делает прокси не совсем правильно. По логике, он даже в случае с LAZY мог бы вначале вытащить DiscriminatorValue из таблицы, определить тип, и сделать свое прокси уже от реального производного типа, т.е. от Sword, а не от абстрактного AbstractWeapon. Но вероятно разработчики Hibernate посчитали что если LAZY то LAZY до конца, и один дополнительный запрос - это будет слишком расточительно. Иначе как это еще можно обьяснить.

вторник, 4 августа 2009 г.

Online Judge unit testing

Задачу 1003 Parity не осилил. Зато придумал тест, на котором мой алгоритм валится.

6
9
1 2 odd
3 4 odd
2 5 odd
2 3 even
1 3 even
2 4 odd
2 3 even
4 5 even
2 3 odd
-1

Правильный ответ 5.

Мне чего-то кажется, что задачи Online Judge неплохо решать используя юнит тестирование. Тоесть, после того как прочитали и поняли условие, не кидаемся сразу же писать код! А спокойно пишем сначала юнит тест. Этот тест должен включать как можно больше разнообразных вариаций входных данных: общие случаи, частые случаи, совсем частные случаи... Также этот тест должен проверять время выполнения и используемую память, чтобы в итоге не превысить пределы установленные в условии задачи. Вот написал себе вспомогательный класс на C# 3.5, который в значительной степени облегчает написание таких юнит тестов:

using System;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace OnlineJudgeUnitTests
{
public abstract class OnlineJudgeProgramTest
{
private static long toBytes(int memoryMB)
{
return memoryMB * 1024 * 1024;
}

private TimeSpan timeLimit;
private long memoryLimit;
private string basePathToDataFiles;

protected OnlineJudgeProgramTest(TimeSpan timeLimit, int memoryLimitMB,
string basePathToDataFiles)
{
this.timeLimit = timeLimit;
memoryLimit = toBytes(memoryLimitMB);
this.basePathToDataFiles = basePathToDataFiles;
}

private string ReadOutput(string outputFile)
{
return new StreamReader(outputFile).ReadToEnd();
}

protected void StandardFileIOTest(string inputFile, string outputFile)
{
inputFile = basePathToDataFiles + inputFile;
outputFile = basePathToDataFiles + outputFile;

StringWriter output = new StringWriter();
DateTime testStartTime = DateTime.Now;

ExecuteProgram(new StreamReader(inputFile), output);

Validate(testStartTime, ReadOutput(outputFile), output.ToString());
}

protected void StandardStringTest(string inputStr, string outputStr)
{
StringWriter output = new StringWriter();
StringReader input = new StringReader(inputStr);
DateTime testStartTime = DateTime.Now;

ExecuteProgram(input, output);

Validate(testStartTime, outputStr, output.ToString());
}

private void Validate(DateTime startTime, String expectedOutput, String realOutput)
{
if (expectedOutput != null)
{
Assert.AreEqual(expectedOutput, realOutput, "Wrong answer");
}

Assert.IsTrue(DateTime.Now - startTime < timeLimit, "Time limit exceeded");
Assert.IsTrue(GC.GetTotalMemory(false) < memoryLimit, "Memory limit exceeded");
}

protected abstract void ExecuteProgram(TextReader input, TextWriter output);
}

}
_Winnie Colorizer

Код не большой и понятный поэтому объяснять не буду. Чтобы скомпилировать его, вам потребуется сборка Microsoft.VisualStudio.QualityTools.UnitTestFramework.

Итак, достаточно всего лишь унаследовать ваш юнит тест от класса OnlineJudgeProgramTest и реализовать метод ExecuteProgram. После чего тест на входном файле PARITY.in и выходном файле PARITY.out будет выглядеть вот так:

      [TestMethod]
public void TestCase4()
{
StandardFileIOTest("PARITY.in", "PARITY.out");
}
_Winnie Colorizer

Не сложно, правда?
Или, если данные для теста генерируются каким-то хитрым образом, то можно воспользоваться методом StandardStringTest:

      [TestMethod]
public void TestCase5()
{
StandardStringTest(CreateSpecialInput(maxArraySize, maxRulesCount), "5000");
}
_Winnie Colorizer

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