2008년 2월 10일 일요일

VMware, Parallels, 그리고 Ubuntu 7.10

VMware와 Parallels trial 버전을 각각 깔아서 시험해본 결과를 적어봅니다.

외장하드를 연결했을 때 Parallels 같은 경우에는 윈도우에도, 맥에도 양쪽 다 제대로 마운트가 안 되는데 비해, VMware에서는 잘 됩니다.

저는 맥에서 네스팟을 쓰는데 Parallels로 윈도우를 시동시키면 인터넷이 안 되네요.

VMware에는 Ubuntu 7.10이 깔리는데 비해 Parallels에는 안 깔립니다. VMware 문서에 보면 Ubuntu 6.10까지만 지원한다고 되어 있어서 7.10이 깔릴지 반신반의 했었는데 깔리더군요. 주의할 점 몇 가지만 적어보면,

Ubuntu 설치할 때 인터넷이 연결된 상태로 하는 게 좋습니다. 그래야 한글 설정 같은 것이 자동으로 잡힙니다. 제가 가진 라이브시디가 초기 버전이라 그런지도 모르지만..

설치가 끝나고 정상적으로 리붓이 되질 않고 멈춰버리는데, VMware에서 강제적으로 리붓시키면 됩니다.

리붓 후 VMware 툴을 설치해야 합니다. Virtual Machine -> Install VMware Tools 를 선택하면 cd-rom 아래에 tar.gz 파일이 보입니다. 아무데로나 복사해서 압축을 풀고(tar -xvzf 파일이름) doc 아래의 Install 문서대로 따라하면 됩니다. 설치는 수퍼유저로 해야 한다고 나와 있는데, Ubuntu를 처음 깔면 root user 패스워드는 설정이 안 되어 있기 때문에 su가 안 먹힙니다. 그 때는 sudo passwd root 로 root user 패스워드를 설정해 주면 됩니다.

처음엔 네이티브에 비해 엄청 느려서 쓸 수가 없다고 생각했는데, VMware 툴을 설치한 후 리붓하니 그래도 적당한 속도가 나오는 것 같습니다. 쓸만하네요.

둘 중 하나를 구입할 생각이었는데 지금으로서는 VMware쪽으로 마음이 기우는 듯 합니다.

2008년 2월 5일 화요일

Buildbot과 Subversion으로 빌드와 테스트 자동화하기 2 - Lisp 프로그램의 빌드와 테스트

쉘에서 명령어 하나로 빌드와 테스트를 다 할 수 있다면 Buildbot을 이용해서 빌드와 테스트를 자동화하는 것은 매우 쉬울 것이다. 내 경우에는 build-and-test.lisp 파일에 빌드와 테스트 과정을 저장하고 쉘에서 다음과 같은 명령어로 빌드와 테스트를 수행하였다.

> sbcl --load build-and-test.lisp

build-and-test 파일의 내용은 대체로 다음과 같이 구성될 수 있을 것이다.

; 이 파일이 로드될 시점의 디렉토리를 asdf:*central-registry*에 동적으로 추가
(push (make-pathname :directory (pathname-directory *load-truename*))
asdf:*central-registry*)
; 어플리케이션과 테스트를 빌드
(require 'application)
(require 'application-test)
; 테스트를 수행
(application-test:run-all-tests)
; Buildbot이 빌드와 테스트가 성공한 것으로 인식하도록 sbcl을 정상 종료
(quit)

사실 asdf를 이용해서 시스템을 구성하고 컴파일하는 것에 대해서는 설명이 조금 길어질 수 있다. 하지만 언제나 예를 보는 것이 가장 쉽기 때문에 내 예를 보이도록 하겠다. SBCLlisp-unit을 사용한 예제이다.

내 디렉토리 구조가 다음과 같다고 할 때,

paip> find . -not -iname "*.fasl" -and -not -iname "*~"
.
./build-and-test.lisp
./paip-test.asd
./paip.asd
./src
./src/chapter-1.lisp
./src/chapter-2.lisp
./src/paip.lisp
./test
./test/chapter-1.lisp
./test/chapter-2.lisp
./test/paip-test.lisp

각각의 파일 내용은 다음과 같다.

paip.asd

;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
(defpackage #:paip-asd
(:use :cl :asdf))

(in-package :paip-asd)

(defsystem paip
:name "paip"
:components ((:module src
:components ((:file "paip")
(:file "chapter-1"
:depends-on ("paip"))
(:file "chapter-2"
:depends-on ("paip"))))))

paip-test.asd

;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-
(defpackage #:paip-test-asd
(:use :cl :asdf))

(in-package :paip-test-asd)

(defsystem paip-test
:name "paip-test"
:depends-on ("paip" :lisp-unit)
:components ((:module test
:components ((:file "paip-test")
(:file "chapter-1"
:depends-on ("paip-test"))
(:file "chapter-2"
:depends-on ("paip-test"))))))

src/paip.lisp

(defpackage #:paip
(:use :cl)
(:documentation
"Exercises of Paradigms of Artificial Intelligence Programming"))

src/chapter-1.lisp

(in-package :paip)

(export '(power count-atoms))

; 1.2
(defun power (base exp)
(if (= exp 0)
1
(do ((result 1 (* result base))
(exp exp (- exp 1)))
((= exp 0) result))))

; 1.3
(defun count-atoms (expression)
(if (null expression)
0
(if (atom expression)
1
(+ (count-atoms (first expression)) (count-atoms (rest expression))))))

src/chapter-2.lisp

(in-package :paip)

(export '(rule-lhs rule-rhs rewrites))

(defun rule-lhs (rule)
"The left-hand side of a rule."
(first rule))

(defun rule-rhs (rule)
"The right-hand side of a rule."
(rest (rest rule)))

(defun rewrites (category grammar)
"Return a list of the possible rewrites for this category."
(rule-rhs (assoc category grammar)))

test/paip-test.lisp

(defpackage #:paip-test
(:use :cl :paip :lisp-unit)
(:export #:paip-test))

(in-package :paip-test)

(defun paip-test ()
(run-all-tests :paip-test))

test/chapter-1.lisp

(in-package :paip-test)

; 1.2
(define-test power
(assert-equal 1 (power 2 0))
(assert-equal 4 (power 2 2))
(assert-equal 1024 (power 2 10)))

; 1.3
(define-test count-atoms
(assert-equal 0 (count-atoms nil))
(assert-equal 1 (count-atoms 'a))
(assert-equal 3 (count-atoms '(1 2 3)))
(assert-equal 5 (count-atoms '((1) 2 (3 (4) 5)))))

test/chater-2.lisp

(in-package :paip-test)

(define-test rule-lhs
(assert-equal (rule-lhs '(noun -> man ball woman)) 'noun))

(define-test rule-rhs
(assert-equal (rule-rhs '(noun -> man ball woman)) '(man ball woman)))

(defmacro with-simple-grammar (&body body)
`(let ((simple-grammar '((sentence -> (noun-phrase verb-phrase))
(noun-phrase -> (article noun))
(verb-phrase -> (verb noun-phrase))
(article -> the a)
(noun -> man ball woman table)
(verb -> hit took saw liked))))
,@body))

(define-test rewrites
(with-simple-grammar
(assert-equal '(man ball woman table) (rewrites 'noun simple-grammar))
(assert-equal '((article noun)) (rewrites 'noun-phrase simple-grammar))))

build-and-test.lisp

(push (make-pathname :directory (pathname-directory *load-truename*))
asdf:*central-registry*)
(require 'paip-test)
(paip-test:paip-test)
(format t "~%")
(quit)

몇 가지 주의할 점을 얘기해보면,
1. 파일과 디렉토리 사이의 의존 관계를 :depends-on 으로 명확히 표시해 주어야 한다. 그저 의존 순서에 따라 순차적으로 적게되면 때때로 패키지 에러가 발생할 수 있다.

2. 파일이 로드되는 순간에 현재 디렉토리를 알아내서 asdf:*central-registry*에 동적으로 추가할 수 있어야 한다. 그래야 체크아웃하는 디렉토리가 어디인지에 영향을 받지 않고 빌드를 수행할 수 있다. 위의 예대로 하면 되지만, 동적으로 디렉토리와 패스를 알아내는 것에 관심이 있다면 이 포스트를 참조하면 좋다.

Buildbot과 Subversion으로 빌드와 테스트 자동화하기

글을 두 개로 나눠서 적는 것이 좋겠다. 일단 여기서는 Subversion과 Buildbot을 어떻게 설치하고 사용하는지 설명한다. 두번째 글에서는 Lisp 코드의 컴파일과 테스트를 ASDF와 Buildbot을 이용해서 어떻게 자동화하는지 설명하겠다.

I. 기본 개념

'단위 테스트, 버전 관리, 빌드와 테스트 자동화' 이 세 가지는 꼭 수행해야 하는 실행지침으로 꼽힌다. 이 글에서는 버전 관리를 위해서 Subversion을 사용하고, 빌드와 테스트 자동화를 위해서 Buildbot을 사용한다고 가정한다.

Subversion은 CVS와 같은 버전 관리 툴이고, CVS의 단점을 보완하기 위해서 개발되었다고 한다. 현재는 가장 널리 쓰이고 있지만, darcs라는 툴로 많은 사람이 넘어가고 있는 것 같기도 하다. (같은 일을 할 때 darcs가 Subversion보다 손이 덜 간다고 한다. darcs는 패치 등에 대한 이론적 기반을 가지고 만들어진 같고, 가장 radical한 언어 중 하나라고 평가되는 Haskell로 짜여졌다.)

Buildbot은 Python으로 작성된, 빌드와 테스트를 자동화하기 위한 도구이다. 버전 관리 도구의 저장소를 감시하고 있다가 저장소에 변화가 생기면 코드를 체크아웃해와서 빌드와 테스트를 수행하고, 그 결과를 웹이나 이메일로 보여준다. Buildbot의 구조적인 장점은 build master와 build slave가 분리되어 있어서, 서로 다른 플랫폼에서 수행되는 빌드들을 관리하기가 용이하다는 것이다. build master는 각각의 build slave들이 어떻게 build를 수행하고 그 결과를 보고해야 할지에 대한 모든 정보를 가지고 있다. 실제로 빌드를 수행하는 build slave들은 build master에 접속해서 build master가 지시하는대로 할 뿐이다. build master와 build slave는 서로 다른 머신에 위치해 있어도 상관이 없기 때문에, 예를 들어 서로 다른 빌드를 수행하는 머신 1,2,3을 머신 4에서 총괄하는 것이 가능하다.

II. 설치

환경이 리눅스라면,
sudo apt-get install subversion
sudo apt-get install buildbot

환경이 맥이라면,
MacPorts를 이용해서 설치하면 된다. MacPorts는 DarwinPorts의 새 이름으로 리눅스의 apt-get과 비슷하게 각종 소프트웨어의 설치를 쉽게 해 준다. MacPorts 홈페이지에서 dmg 파일을 받아 설치하면 되는데, 설치 후 port 명령이 매뉴얼대로 되지 않는다면, 경로 설정이 안 되어 있어서 그런 것이다. 홈디렉토리(~)에 .profile 파일을 만들어서 다음과 같이 경로를 추가한다.

export PATH=/opt/local/bin:/opt/local/sbin:$PATH

'port search 프로그램이름'(e.x. port search buildbot)과 같이 하면 port를 통해 설치할 수 있는 프로그램들을 찾아볼 수 있다. 원하는 프로그램 이름을 찾으면 'sudo port install buildbot'과 같이 인스톨하면 된다.

환경이 윈도우라면,
이 경우는 해보질 않아서 모르겠다. 설치는 각각의 홈페이지를 참조해서 어떻게든 알아서;

III. 사용법

Subversion

저장소를 로컬의 /Users/peter/svn-repos 에 만든다고 할 때 각종 명령의 예는 다음과 같다. 로컬이 아니고 웹인 경우에는 'file://'부분을 'http://'로 바꾸면 된다.

새로운 저장소 만들기
svnadmin create /Users/peter/svn-repos

저장소에 프로젝트 만들기(기존 소스 코드를 저장소 안의 한 디렉토리로 임포트하는 경우)
app> svn import -m "application initial import" file:///Users/peter/svn-repos/app
이 경우는 저장소 아래에 app 디렉토리를 만들고 현재 디렉토리 아래의 모든 디렉토리와 파일들을 집어넣은 것이다. 하지만 저장소의 디렉토리를 살펴봐도 app 디렉토리는 눈에 보이지 않는다. 저장소에 저장된 프로젝트의 디렉토리 위치를 보고 싶다면 어떻게 해야 할까?
1. 체크아웃 해 온 작업본 디렉토리에서 svn info 또는,
2. 아무 곳에서나 svnlook tree /Users/peter/svn-repos

저장소로부터 파일 체크아웃하기
svn co file:///Users/peter/svn-repos/app app

작업본 디렉토리의 상태 알아보기
svn status
파일 앞의 M 표시는 그 파일이 변경되었다는 것을, ? 표시는 그 파일이 버전 관리 하에 놓여있지 않다는 것을 나타낸다. ? 표시된 파일을 버전 관리 대상으로 추가하기 위해서는 svn add를 사용

작업본 디렉토리의 변경을 저장소에 저장
app> svn commit -m "message"

리비전 내역 전체를 보려면
app> svn log

작업본 디렉토리의 내용을 최근 저장소의 리비전으로 갱신하려면
svn update

작업본 디렉토리에서 수행한 변경을 체크아웃 해 온 시점으로 되돌리려면
svn revert .

리비전 10과 리비전 11 사이의 변경 내역을 보고 싶다면
app> svn diff -r 10:11

작업본 디렉토리의 리비전이 11인데 리비전 9로 되돌리고 싶다면
app> svn merge -r 11:9

기타 알아두면 좋은 것

바이너리 파일이나, 백업 파일들은 저장소에 저장될 필요가 없다. Subversion이 이런 파일들을 무시하게 만들기 위해서는 ~/.subversion/config 파일의 global-ignores 부분을 수정하면 된다. 예를 들어, *.fasl 파일들과 *~ 파일들을 무시하고 싶다면 다음과 같이 수정하면 된다.

global-ignores = *.fasl *~

그리고 에디터가 필요한 Subversion 명령을 사용하게 될 경우가 있다. 예를 들어, emacs를 사용하고 싶다고 하면, ~/.profile 에 다음과 같이 추가한다.

SVN_EDITOR=/usr/bin/emacs
export SVN_EDITOR


Buildbot

1. build master를 만들자
/Users/peter/buildbot/master/our-master 에 마스터를 만든다고 가정하자.

buildbot create-master /Users/peter/buildbot/master/our-master

2. build master를 설정한다.
our-master 디렉토리에 보면 master.cfg.sample 파일이 있을 것이다. 이 파일을 편집해서 master.cfg 파일로 저장한다. 예를 보는 게 쉬울 것이다.

# 슬레이브 이름과 패스워드를 설정
from buildbot.buildslave import BuildSlave
c['slaves'] = [BuildSlave("slave-name", "slave-passwd")]

# 슬레이브가 접속할 포트 설정
c['slavePortnum'] = 9989

# 저장소의 변화를 어떻게 감지할지 설정한다. Subversion이라면 SVNPoller를 사용
from buildbot.changes.svnpoller import SVNPoller
c['change_source'] = SVNPoller("file:///Users/peter/svn-repos/app")

# treeStableTimer와 빌더 이름을 설정해 주면 된다.
from buildbot.scheduler import Scheduler
c['schedulers'] = []
c['schedulers'].append(Scheduler(name="all", branch=None,
treeStableTimer=2*60,
builderNames=["our-builder"]))

# 빌드 절차를 팩토리로 만드는 부분. 체크아웃->빌드 및 테스트 순서로 이루어져야 할 것이다.
# svnurl 에 빌드를 위해 체크아웃 해 와야 할 저장소의 디렉토리를 넣는다.
# ShellCommand 로 쉘에서 수행할 명령을 지정할 수 있다.
# 이 경우에는 build-and-test.lisp 파일에 빌드와 테스트 방법을 넣어놓고 로드하게 한 것이다.
from buildbot.process import factory
from buildbot.steps import source, shell
f1 = factory.BuildFactory()
f1.addStep(source.SVN(svnurl="file:///Users/peter/svn-repos/app"))
f1.addStep(shell.ShellCommand(command=["sbcl", "--load", "build-and-test.lisp"]))

# 빌더 이름, 빌더가 사용할 슬레이브 이름과 팩토리 이름을 설정해준다.
# builddir은 슬레이브 디렉토리 아래의 어떤 디렉토리에서 빌드를 수행할지를 설정하는 것이다.
# 이 예에서는 슬레이브가 full이라는 디렉토리를 만들어 빌드를 수행할 것이다.
b1 = {'name': "our-builder",
'slavename': "slave-name",
'builddir': "full",
'factory': f1,
}
c['builders'] = [b1]

c['status'] = []

# 웹과 메일로 빌드의 상태를 볼 수 있다.
from buildbot.status import html
c['status'].append(html.WebStatus(http_port=8010))

# sendToInterestedUsers를 False로 설정하면 extraRecipients 로만 메일이 간다고 한다.
# 빌드가 실패할 때만 메일이 가도록 mode='failing'으로 설정할 수도 있다.
# 하지만 무슨 이유에선지 메일은 제대로 보내지질 않는 것 같다.
from buildbot.status import mail
c['status'].append(mail.MailNotifier(fromaddr="buildbot@localhost", extraRecipients=['peter@matrix'], sendToInterestedUsers=False)

c['buildbotURL'] = "http://localhost:8010/"

3. build slave를 만들자
/Users/peter/buildbot/slave/our-slave 에 슬레이브를 만들고, 위에서 한 설정을 사용한다고 가정하자.

buildbot create-slave /Users/peter/buildbot/slave/our-slave localhost:9989 slave-name slave-passwd

4. build master와 build slave를 실행시키자
buildbot start /Users/peter/buildbot/master/our-master
buildbot start /Users/peter/buildbot/slave/our-slave

build master가 master.cfg 파일을 다시 읽어들이게 하려면
buildbot reconfig /Users/peter/buildbot/master/our-master

master와 slave를 멈추려면
buildbot stop /Users/peter/buildbot/slave/our-slave
buildbot stop /Users/peter/buildbot/master/our-master

이제 localhost:8010 을 살펴보자. 모든 것이 제대로 되었다면 BuildSlave를 클릭했을 때 Slave가 연결된 상태라고 나올 것이다. 저장소에 커밋이 일어나면 treeStableTimer 만큼 기다렸다가 체크아웃과 빌드가 수행된다. 쉘에서 수행된 커맨드의 exit code가 0이 아닌 값이거나 아무 stdout이 없이 20분이 지나면 빌드는 실패한 것으로 간주된다. 그 외의 경우는 성공한 것으로 처리되고, 어떤 경우든 stdout이 로그로 남는다. 웹의 Waterfall Display에서 시간대별로 모든 체크아웃과 빌드의 로그를 볼 수 있다.

여기까지 Subversion과 Buildbot을 살펴보았다. 빌드 과정은 빌드해야 하는 프로그램의 언어가 무엇이냐에 따라서 달라질 수 있다. Lisp 프로그램을 빌드하고 테스트하는 것에 대해서는 다음 글에서 다루도록 하겠다.