星期五, 十一月 30, 2007

python 一个 unit test 代码复用实例及 **kwargs 传递

>>> def func(a, **kwargs):
... print a
... print kwargs
...
>>> def f(a, **kwargs):
... func(a, **kwargs)
...
>>> f(1, x=1)
1
{'x': 1}
注意调用的时候的方式。

在做 Python Tree 的 unit testing 的时候用到了这种方式。因为 node('update', other, ...) 即 Tree.__update__() 方法有一个 ignore_none 参数,所以在测试它的时候必须考虑到。但是否使用 ignore_none=False 的两种情况下,环境的设定是一致的,所以最好能将 ignore_onne=False 作为 **kwargs 传递给一个统一的接口:
class TestUpdate(TestTreeBase):
def doUpdate(self, other, **kwargs):
self.root('update', other, **kwargs)
这时候 doUpdate() 再调用 update 操作的时候就可以使用这种形式了。

之所以要定义一个统一的 doUpdate() 接口是因为 node += other 的形式实际上是调用了 node.__update__(),所以为了增加代码的可重用行,我当然希望 TestUpdate 和 TestIadd 能够尽可能公用代码。因为基本环节的设置都是一样的,唯一不同的是,一个是调用 self.root('update', other),而另一个是调用 self.root += other。

不过 self.root('update', other) 还可以带两个参数 self.root('update', other, ignore_none=False),但 self.root += other 只能带一个参数即 other。

为了使复用成为可能,需要在两个类中分别定义这个统一接口:
class TestUpdate(TestTreeBase):
def doUpdate(self, other, **kwargs):
self.root('update', other, **kwargs)


def _setExistedNodes(self):
self.root.new = "/new"
self.root.new.trunk = "/new/trunk"
self.root.new.trunk.branch = "/new/trunk/branch"
self.root.new.trunk['x'] = "/new/trunk(x)"
self.root.new.branch = "/new/branch"
other = Tree(None)
other.new = None
other.new.trunk = "trunk_updated"
return other

def testUpdateExistedNode(self):
# Update an existed node, both its parent and childs
# should not be affected, nor to other nodes,
# no mather its parent is root or not because the previous tests
# have prove that the root node is same as normal nodes,
temp = self.root
other = self._setExistedNodes()
self.doUpdate(other)
self.assertNodeValue(self.root, "root")
self.assertNodeValue(self.root.new, "/new")
self.assertNodeValue(self.root.new.trunk, "trunk_updated")
self.assertNodeValue(self.root.new.trunk.branch, "/new/trunk/branch")
self.assertNodeIndexValue(self.root.new.trunk, 'x', "/new/trunk(x)")
self.assertNodeValue(self.root.new.branch, "/new/branch")
self.assertPrevious("testBaseCase")
self.failUnless(self.root is temp)

def testUpdateNodeNotIgnoreNone(self):
# Don't ignore "None" setting:
temp = self.root
other = self._setExistedNodes()
self.doUpdate(other, ignore_none=False)
self.assertNodeValue(self.root, None)
self.assertNodeValue(self.root.new, None)
self.assertNodeValue(self.root.new.trunk, "trunk_updated")
self.assertNodeValue(self.root.new.trunk.branch, "/new/trunk/branch")
self.assertNodeIndexValue(self.root.new.trunk, 'x', "/new/trunk(x)")
self.assertNodeValue(self.root.new.branch, "/new/branch")
self.assertPrevious("testBaseCase")
self.failUnless(self.root is temp)
......

class TestIadd(TestUpdate):
def doUpdate(self, other, **kwargs):
self.root += other


def testUpdateNodeNotIgnoreNone(self):
pass

def testUpdateIndexNotIgnoreNone(self):
pass

def testIaddTreeDictForNew(self):
temp = self.root
self.root += {
('new',) : Tree("/new"),
('new', ('x',)) : Tree("/new(x)"),
('new1', ('x', 'y')) : Tree("/new1(x)(y)"),
('new', 'branch', 'data') : Tree(['new', 'branch', 'data'])
}
self.assertNodeValue(self.root.new, "/new")
self.assertNodeIndexValue(self.root.new, 'x', "/new(x)")
self.assertNodeValue(self.root.new1, None)
self.assertNodeIndexValue(self.root.new1, 'x', None)
self.assertNodeIndexValue(self.root.new1['x'], 'y', "/new1(x)(y)")
self.assertNodeValue(self.root.new.branch, None)
self.assertNodeValue(self.root.new.branch.data, ['new', 'branch', 'data'])
self.assertPrevious("testBaseCase")
self.failUnless(self.root is temp)
......
注意两个类中 doUpdate() 接口的定义。对 TestIadd 来说,因为不存在 ignore_none 参数的问题,所以就让 testUpdateNodeNotIgnoreNone 和 testUpdateIndexNotIgnoreNone 直接通过好了。

__package__ = "caxes"
__revision__ = 263

星期五, 十一月 23, 2007

python class methods identity

对于 Python Tree,有个问题我一直很担心,就是每个 Tree instance 会占用多少内存?Python 下面似乎是没有 sizeof() 这样的东西。

最主要的一个问题是,每个 Tree instance 都包含了在 class 中定义的那些方法以及一些全局变量,每个 instance 自身的变量其实只有 _Tree__node_value 和 _Tree__node_items。

这样,我就需要查看一下所有的 instance 的类全局变量和方法是否都使用共享的内存:
>>> import tree
>>> a = tree.Tree(1)
>>> b = tree.Tree(2)
>>> a.__call__ is b.__call__
False
>>> dir(a)
['_Tree__id_visited', '_Tree__node_items', '_Tree__node_value', '_Tree__path_stack', '_Tree__used_names', '__add__', '__call__', '__cmp__', '__contains__', '__doc__', '__getitem__', '__has__', '__iadd__', '__init__', '__islike__', '__issame__', '__module__', '__search__', '__setattr__', '__setitem__', '__str__', '__traverse__', '__update__', '_one_node_get', '_one_node_set']
>>> a._Tree__node_value is b._Tree__node_value
False
>>> a.__dict__
{'_Tree__node_items': {}, '_Tree__node_value': 1}
>>> a._Tree__id_visited is b._Tree__id_visited
True
>>> a.__module__ is b.__module__
True
>>> a.__add__ is b.__add__
False
方法 is identity 检查结果是 false。那么是不是每个 instance 都是以不同的函数栈而并不共享呢?

看一个简单的例子:
>>> class Test:
... var = 1
... def func(self): pass
...
>>> x = Test()
>>> y = Test()
>>> x.var is y.var
True
>>> x.func is y.func
False
>>> id(x.var); id(y.var)
146132400
146132400
>>> id(x.func); id(y.func)
-1208243388
-1208243388
这里 id(x.func) 和 id(y.func) 的返回值一样。根据 help(id):
Help on built-in function id in module __builtin__:

id(...)
id(object) -> integer

Return the identity of an object. This is guaranteed to be unique among
simultaneously existing objects. (Hint: it's the object's memory address.)
它们应该是同一个对象,那为什么 is 检测返回 False 呢?

从 python-chinese@lists.python.cn 上得到了答案:

>>> dir(x.func)
['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__get__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', 'im_class', 'im_func', 'im_self']
>>> id(x.func.im_func)
-1210950828
>>> id(y.func.im_func)
-1210950828
>>> id(y.func.im_self)
-1208531092
>>> id(x.func.im_self)
-1208581588
这也就是说,is 检测到了 im_self 的差异,所以返回 False。而 im_self 其实就是拥有这个 func 的 instance object,而 im 应该就是指 instance method。

不过在 Windows 下运行的结果却是不同的(IDLE):
>>> class Test:
var = 1
def func(self): pass


>>> x = Test()
>>> y = Test()
>>> id(x.var)
11228488
>>> id(y.var)
11228488
>>> id(x.func)
14432856
>>> id(y.func)
14368816

tcpdump -X

前两天公司浏览网页的时候发现总是被插入了东西,IE 的弹出框提示下载什么 ActiveX 控件。从浏览器的"产看源代码"可以看到插入了如下框体
"<"iframe src='http://5.xqhgm.com/2.htm' width=20 height=1">""<"/iframe">"
因为这种问题以前也出现过两次,是由于园区的路由器被攻击,从而相应的 TCP/IP 报文被修改所导致的。这可能导致浏览器去下载病毒,从而危害 Windows 系统;而且有些页面也无法正常访问了,比如 Windows update 页面。

打电话到园区机房去问。推三阻四的,说什么“其他 Windows 路由器的用户都没有报这个问题”云云,实在没有精力去和他们周旋了,而且对于 IDS 或 Linux 网关防毒软件也没什么深入研究,毕竟我也不是安全专家。

我想还是首先证明不是我们自己路由器的问题。当然最彻底的办法是换一块系统硬盘试试看,不过那要中断网络。所以应该用抓包来看看。抓包我用过 snort, ethereal 和 tcpdump,都不是特别熟悉。如果用 tcpdump 简单命令,只能抓取到 TCP/IP 包,对于应用层数据就没有办法了。但是我记得应该是可以用 tcpdump 抓取应用层的数据的,所以 google 了一下,发现可疑用 -x 参数,但得到的都是 hex 十六进制的数据,再 man 一下,查 -x 参数,发现可以用 -X 参数来同时获得 ASCII 数据,于是:
sh# tcpdump -i eth1 -Xls 0 "port 80"
sh# tcpdump -i eth1 -Xls 0 "port 80" | grep xqhgm
然后查一下内网,因为这台机器上自己也有 web apache,所以如果是这个路由器本身有问题,那么我访问它上面的站点的时候,应该也会有问题,并且抓包应该会有结果
sh# tcpdump -i eth0 -Xls 0 "src port 80 and src host 192.168.0.1" | grep xqhgm
但没有抓到东西,而且从其他检查如 ps aux/last 也没有看出异常。

今天再浏览网页,已经没有病毒提示了。

这样看来,tcpdump 是不是也应该可以完成我以前认为只有 snort 才能完成的事情?

使用 mind map 做时间管理

思维导图以前也尝试用过,但似乎不是特别有效果。倒是 XP(eXtreme Programming) 中使用的贴纸法似乎更合我的胃口。不过最近突然发现用思维导图来做计划似乎会是一个比较好的办法。

用 FreeMind,这样在 Linux 下应该也可以用。不过要花点时间看看,很长时间没有在自己的 Linux 桌面环境下用 Java 了。

detect ARP virus by tcpdump

http://www.yourlfs.org/sysadm_zh_CN.html#toc52 中,讨论了两种 ARP 病毒。但是,这次遇到了不同的 ARP 病毒。

伴随症状:作为路由器使用的 Linux 系统变慢,主要是在上面执行命令(远程 ssh)有延迟,但是通过 console tty 直接登入系统却没有发现变慢了,也没有异常的系统负载。而且用 arp 和 arping 却看不出来,必须使用嗅探器或 tcpdump:
[root@localhost ~]# tcpdump -i eth0 "arp"
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes
22:13:04.891192 arp reply 192.168.0.154 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:04.951540 arp reply 192.168.0.155 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:04.990617 arp reply 192.168.0.158 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.036379 arp reply 192.168.0.160 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.081340 arp reply 192.168.0.167 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.112326 arp reply 192.168.0.168 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.141507 arp reply 192.168.0.169 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.191408 arp reply 192.168.0.171 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.203497 arp reply 192.168.0.189 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.236458 arp reply 192.168.0.190 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.266490 arp reply 192.168.0.196 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.336378 arp reply 192.168.0.197 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.353386 arp who-has 192.168.0.199 tell 192.168.0.1
22:13:05.373692 arp reply 192.168.0.198 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.406402 arp reply 192.168.0.199 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.567576 arp reply 192.168.0.200 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.606394 arp reply 192.168.0.211 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.691306 arp reply 192.168.0.212 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.788985 arp reply 192.168.0.222 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.801462 arp reply 192.168.0.230 is-at 00:e0:4d:07:3b:ff (oui Unknown)
......
22:10:42.613320 arp reply 192.168.0.130 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613348 arp reply 192.168.0.132 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613376 arp reply 192.168.0.133 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613407 arp reply 192.168.0.134 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613437 arp reply 192.168.0.137 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613467 arp reply 192.168.0.138 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613497 arp reply 192.168.0.141 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613526 arp reply 192.168.0.142 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613556 arp reply 192.168.0.143 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613586 arp reply 192.168.0.144 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613615 arp reply 192.168.0.145 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613645 arp reply 192.168.0.149 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613673 arp reply 192.168.0.151 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613704 arp reply 192.168.0.152 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613733 arp reply 192.168.0.153 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613764 arp reply 192.168.0.154 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613792 arp reply 192.168.0.155 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613823 arp reply 192.168.0.158 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613853 arp reply 192.168.0.160 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613884 arp reply 192.168.0.167 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613912 arp reply 192.168.0.168 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613943 arp reply 192.168.0.169 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613973 arp reply 192.168.0.171 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614011 arp reply 192.168.0.189 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614362 arp reply 192.168.0.190 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614394 arp reply 192.168.0.196 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614424 arp reply 192.168.0.197 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614455 arp reply 192.168.0.198 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614484 arp reply 192.168.0.199 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614515 arp reply 192.168.0.200 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614545 arp reply 192.168.0.211 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614576 arp reply 192.168.0.212 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614606 arp reply 192.168.0.222 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614637 arp reply 192.168.0.230 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614667 arp reply 192.168.0.250 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614697 arp reply 192.168.0.253 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614727 arp reply 192.168.0.254 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614758 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614877 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.615186 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.615788 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.616564 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.682754 arp who-has 192.168.0.98 tell 192.168.0.125
22:10:43.361718 arp who-has 192.168.0.101 tell 192.168.0.5
22:10:43.368052 arp who-has 192.168.0.199 tell 192.168.0.1

8192 packets captured
17121 packets received by filter
736 packets dropped by kernel

星期一, 十一月 19, 2007

python further than "unittest by permutation"

python Permutation 和 unittest 耗时测试隔离 中,讨论了使用排列来进行更全面的一个测试。这种方法的一个问题是耗时太长,P(6, 6)=720 耗时 ~45s,最好的情况也需要 ~30s,则 P(7, 7) 则需要耗时最少 ~3.5min,>P(8, 8) 看上去就让人无法接受了。所以在代码中增加一个判断:
    cond_len = len(names)
if cond_len > 7:
raise "Too many tests by permutation will be too time consuming."
接下来,我需要对 Tree.__call__() 和 Tree._one_node_set() 方法进行测试,对 __call__(),主要是看 value = root.trunk.node() 这种操作能否正确取值,以及 root.trunk.node('set', value) 能否正确赋值;而对 _one_node_set() 此时它涉及到的测试就比较多了,因此不适合于使用排列方法来做全面的测试。

同时,因为已经使用排列方法对 attribute 和 item 类型的赋值和取值进行过测试了(TestSetAttr 和 TestKeyIndex),那么只有能够保证它们的验证结论可以在这里直接使用,就可以使用另外一种方法来解决"始终保证后面的操作不会对前面的已有的树节点造成破坏"这样一个需求。

那么首先定义一个基类:
class TestTreeBase(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()

def assertNodeValue(self, node, value):
self.failUnless(isinstance(node, Tree))
self.failUnlessEqual(node(), value)
self.failUnless(node() is node._Tree__node_value)
# This identity test is very important !!!
# it proves the results of the previous TestCases
# can be applied directly to the latter tests from now on.

def assertNodeIndexValue(self, node, key, value):
try:
indexed_node = node[key]
except KeyError:
self.fail("__getitem__ operator raises an unexpected KeyError")
self.failUnless(node[key] is node._Tree__node_items[key])
# Important identity test !!!
self.assertNodeValue(node[key], value)
这里对 node() 进行验证,而不像 TestTree 那样对 node._Tree__node_value 进行验证,但需要保证 node() 就是 node._Tree__node_value,因此是一个 identity 检查。

对 __call__() 方法进行测试,因为其 test* 方法比较少,所以仍然可以使用排列来进行。但对于 TestOneNodeSet,必须使用新方法。

我最初想到的一个办法是在每次调用一个 _set* 的时候,先调用"前面"的那个 _set* 方法,并在每个 test* 方法中调用"前面"那个 test* 方法(使用引号是因为 PyUnit 的各个 tests 之间实际上并不存在顺序,顺序只能由自己来定义,因此我按照编辑上从上到下的顺序来排列)。则代码象这样:
class TestOneNodeSet(TestTreeBase):
...
def _setCreateAttr(self):
self.root._one_node_set(('node_1',), 2)
self.root._one_node_set(['node_2'], "2")

def testCreateAttr(self):
if self._setToggle:
self._setCreateAttr()
self.assertNodeValue(self.root.node_1, 2)
self.assertNodeValue(self.root.node_2, "2")
# If the target node is assigned a Tree instance
self.failUnlessRaises(TreeExc, self.root._one_node_set, ('assign_Tree_directly',), Tree("two"))
self.assertPrevious("testBaseCase")

def _setCreateAttrWhenParentExisted(self):
self._setCreateAttr()
self.root.existed_parent = "existed_parent"
self.root.existed_parent.node1 = "existed/node1"
self.root._one_node_set(('existed_parent', 'node2'), "existed/node2")

def testCreateAttrWhenParentExisted(self):
# The parent nodes should not be affected
if self._setToggle:
self._setCreateAttrWhenParentExisted()
self.assertNodeValue(self.root.existed_parent, "existed_parent")
# Make sure the previous nodes are not affected, because Tree._one_node_set() is a recursive operation
self.assertNodeValue(self.root.existed_parent.node1, "existed/node1")
self.assertNodeValue(self.root.existed_parent.node2, "existed/node2")
self.assertPrevious("testCreateAttr")
assertPrevious() 是在 TestTree 中定义的:
class TestTree(unittest.TestCase):
...
def assertPrevious(self, test_name):
self._setToggle = 0
test_method = getattr(self, test_name)
test_method()
self._setToggle = 1
这样有几个问题,一是测试方法之间的模块性就没有那么好了,另一方面,一个 test* 方法每次调用"前面"那个 test*,而那个 test* 又要调用"它自己前面"的那个 test*,则测试方法需要耗费的时间也是会越来越长,虽然还是在一个可接受的范围之类,但似乎每次都测前面的有点多余。

实际上,完全可以在 setUp() 中就先建立所有应该已存在节点,因为 node() 已经做了 is identity 检查,所以每次只需要测试这些节点,实际上就表示已经对所有已存在的树节点进行了检查。

编辑代码如下:
class TestOneNodeSet(TestTreeBase):
def _setBaseCase(self):
# Since the strategy is changed,
# build some previous nodes first:
TestTreeBase._setBaseCase(self)
self.root['base'] = "(base)"
self.root['base'].data = "(base)/data"
self.root['base']['x'] = "(base|x)"
self.root['base']['x'].extra = "(base|x)/extra"
self.root['base']['x']['y'] = "(base|x|y)"
self.root.base = "base"
self.root.base['x'] = "base(x)"
self.root.base['x'].data = "base(x)/data"
self.root.base['x']['y'] = "base(x|y)"
self.root.base['x']['y'].extra = "base(x|y)/extra"
self.root.base['x']['y']['z'] = "base(x|y|z)"

def testBaseCase(self):
if self._setToggle:
self._setBaseCase()
# TestTreeBase.testBaseCase(self)
self.assertNodeIndexValue(self.root, 'base', "(base)")
self.assertNodeValue(self.root['base'].data, "(base)/data")
self.assertNodeIndexValue(self.root['base'], 'x', "(base|x)")
self.assertNodeValue(self.root['base']['x'].extra, "(base|x)/extra")
self.assertNodeIndexValue(self.root['base']['x'], 'y', "(base|x|y)")
self.assertNodeValue(self.root.base, "base")
self.assertNodeIndexValue(self.root.base, 'x', "base(x)")
self.assertNodeValue(self.root.base['x'].data, "base(x)/data")
self.assertNodeIndexValue(self.root.base['x'], 'y', "base(x|y)")
self.assertNodeValue(self.root.base['x']['y'].extra, "base(x|y)/extra")
self.assertNodeIndexValue(self.root.base['x']['y'], 'z', "base(x|y|z)")

def _setCreateAttr(self):
self.root._one_node_set(('node_1',), 2)
self.root._one_node_set(['node_2'], "2")

def testCreateAttr(self):
if self._setToggle:
self._setCreateAttr()
self.assertNodeValue(self.root.node_1, 2)
self.assertNodeValue(self.root.node_2, "2")
# If the target node is assigned a Tree instance
self.failUnlessRaises(TreeExc, self.root._one_node_set, ('assign_Tree_directly',), Tree("two"))
self.assertPrevious("testBaseCase")

def _setCreateAttrWhenParentExisted(self):
# self._setCreateAttr()
self.root.existed_parent = "existed_parent"
self.root.existed_parent.node1 = "existed/node1"
self.root._one_node_set(('existed_parent', 'node2'), "existed/node2")

def testCreateAttrWhenParentExisted(self):
# The parent nodes should not be affected
if self._setToggle:
self._setCreateAttrWhenParentExisted()
self.assertNodeValue(self.root.existed_parent, "existed_parent")
# Make sure the previous nodes are not affected, because Tree._one_node_set() is a recursive operation
self.assertNodeValue(self.root.existed_parent.node1, "existed/node1")
self.assertNodeValue(self.root.existed_parent.node2, "existed/node2")
# self.assertPrevious("testCreateAttr")
self.assertPrevious("testBaseCase")

...
setUp() 方法是在 TestTreeBase 中定义的:
class TestTreeBase(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()
此时,注意 def _setCreateAttrWhenParentExisted(self): 中 self._setCreateAttr() 已经被注释,而 def testCreateAttrWhenParentExisted(self): 中也不再使用 self.assertPrevious("testCreateAttr"),而代之以 self.assertPrevious("testBaseCase") 即可。

__package__ = "caxes"
__revision__ = [259:262]

symlink for Apache DocumentRoot/Directory

/home/httpd 是 /data/httpd 的符号链接。在 httpd.conf 中,一开始使用的都是 DocumentRoot /home/httpd/$site 这样的形式,这次配置一个简单的认证,采用如下方法:
Directory "/data/httpd/$site"
AuthUserFile /usr/local/apache2/conf/.htpasswd
AuthName "SimpleAuth"
AuthType Basic
require valid-user
Options None
AllowOverride None
Order Deny,Allow
Deny from all
Allow from $ipaddr
结果无法通过验证。

把 Directory "/data/httpd/$site" 改为 Directory "/home/httpd/$site" 就可以了。

如果 /data/httpd/$site/ 下的一个页面通过 url 连接到 http://$site/ 下的某个其他页面则仍会有问题,因为显然不能自动提交认证信息。

星期二, 十一月 13, 2007

python Permutation 和 unittest 耗时测试隔离

python unittest TestSuite 框架实践和几个问题中,提到"按照任意的顺序去调用 _set* 和 test* 方法,始终保证后面的操作不会对前面的已有结果造成破坏"这样一个需求,当时并没有给出一个结论。

实际上,这里涉及到一个组合数的问题。比如在 test_tree.TestSetAttr 中,有这样几个带 _set* 的测试:
["testCreateNew", "testAssignNonTreeToExisted", "testAssignTreeToExisted", "testReserveSomeAttrWhenReplaceNode"]。这里 TestSetAttr 虽然是从 TestTree 继承来的,包含 testBaseCase() 方法,但因为 testBaseCase() 是基础,在 setUp() 里面设定,所以应该排除。这样一来,就有 P(4, 4)=24 种组合情况。

为了实现这一点,首先编写一个简单的组合数函数:test/caxes/support.py:
def full_permutate(items):
if len(items) <=1:
yield items
else:
for P in full_permutate(items[1:]):
for i in range(len(P) + 1):
yield P[:i] + items[0:1] + P[i:]
可以定义相应的单元测试 test/caxes/test_support.py:
#!/usr/bin/python
# -*- encoding: utf-8 -*-

__author__ = "Roc Zhou #周鹏"
__date__ = "12 November 2007"
__version__ = "0.2"
__license__ = "GPL v2.0"

import unittest

from support import full_permutate

class TestPermute(unittest.TestCase):
def testFullPermutate(self):
self.failUnlessEqual([ x for x in full_permutate("") ], [""])
self.failUnlessEqual([ x for x in full_permutate([]) ], [[]])
self.failUnlessEqual([ x for x in full_permutate(['a']) ], [['a']])
self.failUnlessEqual([ x for x in full_permutate(['a', 'b']) ], [['a', 'b'], ['b', 'a']])
result = []
for x in full_permutate(['a', 'b', 'c']): result.append(x)
expect = [
['a', 'b', 'c'],
['a', 'c', 'b'],
['b', 'a', 'c'],
['b', 'c', 'a'],
['c', 'a', 'b'],
['c', 'b', 'a'] ]
result.sort()
expect.sort()
self.failUnlessEqual(result, expect)
result = []
for x in full_permutate(['a', 'b', 'c', 'd']): result.append(x)
expect = [
['a', 'b', 'c', 'd'],
['a', 'b', 'd', 'c'],
['a', 'c', 'b', 'd'],
['a', 'c', 'd', 'b'],
['a', 'd', 'b', 'c'],
['a', 'd', 'c', 'b'],
['b', 'a', 'c', 'd'],
['b', 'a', 'd', 'c'],
['b', 'c', 'a', 'd'],
['b', 'c', 'd', 'a'],
['b', 'd', 'a', 'c'],
['b', 'd', 'c', 'a'],
['c', 'a', 'b', 'd'],
['c', 'a', 'd', 'b'],
['c', 'b', 'a', 'd'],
['c', 'b', 'd', 'a'],
['c', 'd', 'a', 'b'],
['c', 'd', 'b', 'a'],
['d', 'a', 'b', 'c'],
['d', 'a', 'c', 'b'],
['d', 'b', 'a', 'c'],
['d', 'b', 'c', 'a'],
['d', 'c', 'a', 'b'],
['d', 'c', 'b', 'a'] ]
result.sort()
expect.sort()
self.failUnlessEqual(result, expect)

if __name__ == "__main__":
unittest.main()
关于 Python 的排列组合,有不少参考资料,讨论的人也很多。例如可以参考:
http://snippets.dzone.com/posts/show/753
http://www.pyzen.cn/subject/2966/
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/474124

然后,在 test_tree.TestTree 中增加这样的定义:

from caxes import support

class TestTree(unittest.TestCase):
...
def _testPrevious(self, test_name):
self._setToggle = 0
test_method = getattr(self, test_name)
test_method()
self._setToggle = 1

def testPermutation(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
name = member[0]
if name.startswith("_set"): names.append(name[4:])
names.remove("BaseCase")
cond_len = len(names)
for cond in support.full_permutate(names):
self.tearDown()
self.setUp()
previous_names = ["testBaseCase"]
for i in range(cond_len):
test_name = "test%s" % cond[0]
testMethod = getattr(self, test_name)
testMethod()
for pre_test_name in previous_names:
self._testPrevious(pre_test_name)
previous_names.append(test_name)

...
if __name__ == "__main__":
# #2:
import __main__
suite = unittest.TestLoader().loadTestsFromModule(__main__)
unittest.TextTestRunner(verbosity=2).run(suite
这样运行时可以的。但是有一个问题,在后面的 TestKeyIndex 测试中,_set* 方法有 6 个,则 P(6, 6)=720 中排列,这意味着需要花比较长的时间才能完成一次测试,在一台 2.4GHz*2 Xeon, 1G MEM 的主机上,花费了 ~45s。

《Pragmatic Unit Testing》中提到要讲耗时的测试隔离。那么在这里具体应该怎么做呢。设定条件从 tests list 删除实际上不太现实:
suite = unittest.TestLoader().loadTestsFromTestCase(TestKeyIndex)
# for test in suite: print test
# suite._tests.remove(TestKeyIndex("testUnordered"))
remove() 最终会抛出 IndexError。而且这样又会破坏正交性。

也许有很多解法。目前我使用一个类 TestTreeComplex 从 TestTree 集成,将 testPermutation() 只定义到 TestTreeComplex 中,同时定义 TestSetAttrComplex 和 TestKeyIndexComplex 从 TestTreeComplex 和 TestSetAttr/TestKeyIndex 双重集成:
sh# vi test/test_tree_complex.py
#!/usr/bin/python
# -*- encoding: utf-8 -*-

__author__ = "Roc Zhou #周鹏"
__date__ = "13 November 2007"
__version__ = "0.2"
__license__ = "GPL v2.0"

import unittest
import inspect

from caxes import support
import test_tree

import tree
from tree import Tree,TreeExc,TreeTypeExc,TreePathConvExc

class TestTreeComplex(test_tree.TestTree):
def _testPrevious(self, test_name):
self._setToggle = 0
test_method = getattr(self, test_name)
test_method()
self._setToggle = 1

def testPermutation(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
name = member[0]
if name.startswith("_set"): names.append(name[4:])
names.remove("BaseCase")
cond_len = len(names)
for cond in support.full_permutate(names):
self.tearDown()
self.setUp()
previous_names = ["testBaseCase"]
for i in range(cond_len):
test_name = "test%s" % cond[0]
testMethod = getattr(self, test_name)
testMethod()
for pre_test_name in previous_names:
self._testPrevious(pre_test_name)
previous_names.append(test_name)

class TestSetAttrComplex(TestTreeComplex, test_tree.TestSetAttr):
pass

class TestKeyIndexComplex(TestTreeComplex, test_tree.TestKeyIndex):
pass

if __name__ == "__main__":
suite = unittest.TestSuite()
suite.addTest(TestSetAttrComplex("testPermutation"))
suite.addTest(TestKeyIndexComplex("testPermutation"))
unittest.TextTestRunner(verbosity=2).run(suite)
这里 TestSetAttrComplex 和 TestKeyIndexComplex 实际上不需要定义任何其他方法和属性。这样可以有效隔离耗时测试,将其放到项目自动化构建的每时或每日构建中。

__package__ = "caxes"
__revision__ = [258:~]

python distutils 调整目录结构后

项目自动化中谈到对整个目录结构进行调整以利于测试和项目自动化构建,则相应的,distutils 的 setup.py 也需要相应的进行调整。

现在以完成部分的目录结构是这样的:
trunk/
ChangeLog
lib/
tree.py
LICENSE
MANIFEST.in
README
setup.py
test/
caxes/
__init__.py
support.py
test_support.py
test_tree.py
test_tree_complex.py
test_tree.bk
则调整后的 setup.py 为:
#!/usr/bin/python
# -*- encoding: utf-8 -*-

__author__ = "Roc Zhou #周鹏"
__date__ = "13 November 2007"
__version__ = "0.2"
__license__ = "GPL v2.0"

from distutils.core import setup
from distutils import sysconfig

lib_prefix = sysconfig.get_python_lib()

setup(
name = 'caxes',
version = '0.2',
description = """
Some new Python data types such as Tree,
and configuration sharing mechanism implementation.
""",
long_description = """
Some new Python data structure,
can be afforded as APIs for new ways of configuration,
and configuration sharing mechanism implementation.

It's a subproject of uLFS.

uLFS means "Your Manageable, Automatable and Templated Reusable Linux From Scratch",
it's a set of tools to build your own customed Linux distribution with
more managability than raw LFS(Linux From Scratch). Include source package
manager, file system backup and realtime mirror apps, and some assistant data
structure such as Tree writen in Python, etc...
""",
author = "Roc Zhou",
author_email = 'chowroc.z@gmail.com',
platforms = "Platform Independent",
license = "GPL v2.0",
url = "http://crablfs.sourceforge.net",
download_url = "http://sourceforge.net/projects/crablfs",
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License (GPL)",
"Natural Language :: English",
"Natural Language :: Chinese (Simplified)",
"Operating System :: POSIX",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Topic :: Software Development :: Libraries :: Python Modules"
],
py_modules = ["tree"],
package_dir = {"" : "lib", "caxes" : "lib/caxes"},
# data_files = [("%s/test" % lib_prefix, ["test/test_tree*.py", "test/support.py"])],
# data_files = [("%s/test" % lib_prefix, ["test/*.py"]), ("%s/test/caxes" % lib_prefix, ["test/caxes/*.py"])],
data_files = [("%s/test" % lib_prefix, ["test/*.py", "test/caxes"])],
# packages = ['caxes']
)
这里先说明一下 py_modules 和 package_dir 的调整。

之前的定义为:
py_modules = ["tree"]
package_dir = {"caxes" : "lib"}
因为目录结构为:
trunk/
tree.py
lib/
...
但当目录结构调整后,会提示找不到模块 tree 的文件 tree.py,因为这个文件已经不再 trunk 的根目录下了,而是移到了 lib/,此时应该调整 package_dir,增加 "" : "lib"。而原来的 lib/ 变成了 lib/caxes,所以 package_dir 也应该相应变动。

在下面定义 data_files,保证 test/test_tree*.py 被安装到 /usr/lib/python2.4/site-packages/test/,而 test/caxes/ 被拷贝成 /usr/lib/python2.4/site-packages/caxes/,忽略掉 *.bk 文件。此时必须记住要调整 trunk/ 下的 MANIFEST.in 文件:
include *.py
include test/*.py
recursive-include test/caxes *
include README
include ChangeLog
include LICENSE
注意 recursive-include 一行,否真 test/caxes 目录不会被拷贝。

有一点比较奇怪的是,当我使用:
data_files = [("%s/test" % lib_prefix, ["test/test_tree*.py", "test/caxes"])]
却提示找不到文件 test/test_tree*.py。这是为什么呢?

__package__ = "caxes"
__revision__ = [258:259]

星期六, 十一月 10, 2007

python unittest TestSuite 框架实践和几个问题

之前的 python Tree 实现中单元测试做的不好,在"Progmatic Unit Testing", 心得和自省中说过这一点。现在要重写单元测试。

为了保证所有测试的独立性和正交性,编写测试 setattr() 操作正确性的代码如下:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

__author__ = "Roc Zhou #周鹏"
__date__ = "09 November 2007"
__version__ = "0.2"
__license__ = "GPL v2.0"

"""Unittest for tree.Tree"""

from gettext import gettext as _

import os
import unittest
import inspect

import tree
from tree import Tree,TreeExc,TreeTypeExc,TreePathConvExc

# def _setAll(self):
# names = []
# for member in inspect.getmembers(self, inspect.ismethod):
# method_name = member[0]
# if method_name.startswith("_set"):
# names.append(method_name)
# names.remove("_setAll")
# for name in names:
# method = getattr(self, name)
# method()

# def testPreviousAll(self):
# # """The previous Tree nodes should not be affected by the latter operations"""
# self._setToggle = 0
# names = []
# for test_name in inspect.getmembers(self, inspect.ismethod):
# if test_name.startswith("test"):
# names.append(test_name)
# names.remove("testPreviousAll")
# # #1:
# suite = unittest.TestSuite(map(self.__class__, test_names))
# suite.run()
# # #2:
# # for name in names:
# # test_method = getattr(self, name)
# # test_method()

class TestTree(unittest.TestCase):
def setUp(self):
self._setToggle = 1
### self._setAll = _setAll
### self.testPreviousAll = testPreviousAll
### Why can't ??????

def tearDown(self):
self.root = None

def assertNodeSet(self, node, value):
self.failUnless(isinstance(node, Tree))
self.failUnlessEqual(node._Tree__node_value, value)

def assertNodeValue(self, node, value):
self.failUnless(isinstance(node, Tree))
self.failUnlessEqual(node(), value)

def assertNodeIndex(self, node, key, value):
self.failUnless(node._Tree__node_items.has_key(key))
self.failUnless(isinstance(node[key], Tree))
self.failUnlessEqual(node[key]._Tree__node_value, value)

def assertNotNodeIndex(self, node, key):
self.failIf(node._Tree__node_items.has_key(key))

def assertNodeIndexValue(self, node, key, value):
self.failUnless(node.has_key(key))
self.assertNodeValue(node[key], value)

def _setBaseCase(self):
self.root = Tree("root", data="root.data", extra="extra")

def testBaseCase(self):
# """The simplest assignment should create a Tree instance with several valid sub nodes"""
if self._setToggle:
self._setBaseCase()
self.assertNodeSet(self.root, "root")
self.assertNodeSet(self.root.data, "root.data")
self.assertNodeSet(self.root.extra, "extra")

def _setAll(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
method_name = member[0]
if method_name.startswith("_set"):
names.append(method_name)
names.remove("_setAll")
for name in names:
method = getattr(self, name)
method()

def testPreviousAll(self):
# """The previous Tree nodes should not be affected by the latter operations"""
self._setToggle = 0
# self.tearDown()
# self.setUp()
self._setAll()
names = []
for member in inspect.getmembers(self, inspect.ismethod):
test_name = member[0]
if test_name.startswith("test"):
names.append(test_name)
names.remove("testPreviousAll")
# #1:
# suite = unittest.TestSuite(map(self.__class__, names))
# suite.run(suite)
# #2:
for name in names:
test_method = getattr(self, name)
test_method()

class TestSetAttr(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()

def testNameReserved(self):
# """Assign to reserved names should be avoided"""
__used_names = object.__dict__.copy().keys() + [
'_Tree__path_stack', '_Tree__id_visited', '_Tree__used_names',
'_Tree__node_value', '_Tree__node_items',
'__path_stack', '__id_visited', '__used_names', '__node_value', '__node_items',
'__getattr__', '__setitem__', '__getitem__', '__call__',
'__add__', '__iadd__', '__cmp__',
'_one_node_set', '__traverse__', '__update__', '__copy__', '__search__'
]
for used_name in __used_names:
try:
setattr(self.root, used_name, 1)
self.fail(_("A reserved name '%s' should can not be reassignable" % used_name))
except TreeExc:
pass
except TypeError:
pass

def _setCreateNew(self):
self.root.trunk = 1
self.root.branch = Tree(None, data='branch/data', extra=('branch', 'extra'))

def testCreateNew(self):
# """If the sub node does not exist, assign it directly"""
if self._setToggle:
self._setCreateNew()
self.assertNodeSet(self.root.trunk, 1)
self.assertNodeSet(self.root.branch, None)
self.assertNodeSet(self.root.branch.data, "branch/data")
self.assertNodeSet(self.root.branch.extra, ('branch', 'extra'))

def _setAssignNonTreeToExisted(self):
self.root.nt_ex_simple = 1
self.root.nt_ex_complex = Tree(1, data="nt_ex_complex/data", extra="nt_ex_complex/extra")
# Create new first
self.root.nt_ex_simple = "one"
self.root.nt_ex_complex = "ONE"

def testAssignNonTreeToExisted(self):
# """If attribute is an existed Tree, and target is not a Tree, only the node value should be replaced"""
if self._setToggle:
self._setAssignNonTreeToExisted()
self.assertNodeSet(self.root.nt_ex_simple, "one")
self.assertNodeSet(self.root.nt_ex_complex, "ONE")
# If the node has childs, only value replacement:
self.assertNodeSet(self.root.nt_ex_complex.data, "nt_ex_complex/data")
self.assertNodeSet(self.root.nt_ex_complex.extra, "nt_ex_complex/extra")

def _setAssignTreeToExisted(self):
self.root.tr_ex_simple = 2
self.root.tr_ex_complex = Tree(2, data="tr_ex_complex/data")
self.root.tr_ex_simple_branch = '2'
self.root.tr_ex_complex_branch = Tree(2, data="tr_ex_complex_branch/data")
# Create first
self.root.tr_ex_simple = Tree("two")
self.root.tr_ex_complex = Tree("TWO")
self.root.tr_ex_simple_branch = Tree("_two", data="tr_ex_simple_branch/data")
self.root.tr_ex_complex_branch = Tree("_TWO", extra="tr_ex_complex_branch/extra")

def testAssignTreeToExisted(self):
# """If attribute is an existed Tree, and target is a Tree too, the node itself should be replaced"""
if self._setToggle:
self._setAssignTreeToExisted()
# (1) target Tree instance does not have attributes, and original Tree does not have sub nodes:
self.assertNodeSet(self.root.tr_ex_simple, "two")
# (2) target Tree instance does not have attributes, and original Tree have sub nodes:
self.assertNodeSet(self.root.tr_ex_complex, "TWO")
self.failIf(hasattr(self.root.tr_ex_complex, "data"))
# (3) target Tree instance has attributes, and original Tree does not have sub nodes:
self.assertNodeSet(self.root.tr_ex_simple_branch, "_two")
self.assertNodeSet(self.root.tr_ex_simple_branch.data, "tr_ex_simple_branch/data")
# (4) target Tree instance has attributes, and original Tree have sub nodes too:
self.assertNodeSet(self.root.tr_ex_complex_branch, "_TWO")
self.failIf(hasattr(self.root.tr_ex_complex_branch, "data"))
self.assertNodeSet(self.root.tr_ex_complex_branch.extra, "tr_ex_complex_branch/extra")

def _setReserveSomeAttrWhenReplaceNode(self):
self.root.replace_but_reserve_1 = ["Replace", "But", "Reserve", "Method", 1]
self.root.replace_but_reserve_1.br1 = 1
subtree = self.root.replace_but_reserve_1.br1
self.root.replace_but_reserve_1 = Tree("replaced_by_method_1", data="another_1")
self.root.replace_but_reserve_1.br1 = subtree

self.root.replace_but_reserve_2 = ["Replace", "But", "Reserve", "Method", 2]
self.root.replace_but_reserve_2.br2 = 2
self.root.replace_but_reserve_2 = Tree("replaced_by_method_2", br2=self.root.replace_but_reserve_2.br2, data="another_2")

def testReserveSomeAttrWhenReplaceNode(self):
if self._setToggle:
self._setReserveSomeAttrWhenReplaceNode()
self.assertNodeSet(self.root.replace_but_reserve_1, "replaced_by_method_1")
self.assertNodeSet(self.root.replace_but_reserve_1.br1, 1)
self.assertNodeSet(self.root.replace_but_reserve_1.data, "another_1")
self.assertNodeSet(self.root.replace_but_reserve_2, "replaced_by_method_2")
self.assertNodeSet(self.root.replace_but_reserve_2.br2, 2)
self.assertNodeSet(self.root.replace_but_reserve_2.data, "another_2")

......
这里首先从 TestCase 继承一个 TestTree 基类,并自定义一些 assert 测试方法来做一些基本的测试。这样测试 setattr() 的类 TestSetAttr 可以从 TestTree 继承并直接调用这些 assert 来完成更复杂的测试,这样也就提高了复用性。

每一个 test* 方法基本上和一个 _set* 方法对应,并只进行很简单很专门的测试,这样就可以将方法名定义得更具有可读性,而且方便其他测试方法来调用,test* 和 _set* 分开也是为了这个目的。

比如,上面在 TestTree 中定义的 testPreviousAll() 方法,就是为了测试之前所有的操作是否互相影响,比如对一个节点的子节点进行操作之后,它本身的值、它的兄弟节点和其他子节点都不应该受到影响,它会利用 inspect 模块提供的功能寻找所有自己这个测试类中的 test* 方法(排除自身),并逐一调用,以确保这一点。因为 TestSetAttr 是从 TestTree 继承的,所以它也会有这个 testPreviousAll() 方法。

要逐一调用这些方法方法,有两种想法,其一是利用 getattr() 得到这个方法的实例,并直接调用;另一种思路是利用 TestSuite 的构造方法得到一个 test suite,并调用 suite.run() 直接运行。

除了利用 testPreviousAll() 方法来做这件事外,我一开始的另一个思路是在后面调用 TextTestRunner().run(suite) 的时候调用对 TestSetAttr 前后调用两次,而不用使用 testPreviousAll() 方法。

无论使用那种方法,前后两次调用 test* 方法都需要分别设定 _setToggle 标志,保证第二次调用的时候,test* 方法不会去调用 _set* 重复设定各个节点,这样才能得到正确的测试结果。

但是这几种思路都有问题。逐一来讨论。

首先,更深入的了解一下 unittest 的框架。TestCase 为一个基本单元,由若干 TestCase 组成一个 TestSuite,是可以运行的单元,TestSuite 也可以包含 TestSuite。但必须通过调用 TestRunner 的实例来运行 TestSuite 的实例。例如:
suite = unittest.TestSuite()
suite.addTest(...)
...
unittest.TextTestRunner(verbosity=2).run(suite)
在 Python 的官方手册中,提到一个 test fixture,这个 fixture 事实上并没有相应的 class,只不过是一种概念,即可以将若干包含 TestSuite 的 TestSuite 看作一个 fixture,因为其环境比较复杂了。如何对这样一个 fixture 设置 setUp() 和 tearDown() 环境这里不讨论。

为了运行所有这些测试,最简单的办法是直接调用 unittest.main() 函数:
if __name__ == "__main__":
unittest.main()
它会自动去寻找当前模块里所有的 tests 组成 TestSuite 并运行之。因此这段代码的等效代码可以看作是:
if __name__ == "__main__":
import __main__
suite = unittest.TestLoader().loadTestsFromModule(__main__)
unittest.TextTestRunner(verbosity=2).run(suite)
所以问题的关键是如何得到这些 TestSuite 并将 tests 加入其中。

除了调用 unittest.main() 以外,显式地创建 test suite 有很多方法,除了上面的 TestLoader 之外,还可以逐个加入:
suite = unittest.TestSuite()
suite.addTest(TestSetAttr("testNameReserved"))
suite.addTest(TestSetAttr("testCreateNew"))
...
unittest.TextTestRunner(verbosity=2).run(suite)
通过阅读 unittest 的源代码可以知道,addTest() 的参数即为一个 test,也就是一个 TestCase 的 instance,并且这个 instance 的 testMothod() 方法指向传递给构造函数的方法名所表示的方法。

TestSuite.addTests() 的参数是 tuple list of tests。

另一种方法是:
test_names = [
"testNameReserved",
"testCreateNew",
"testAssignNonTreeToExisted",
"testAssignTreeToExisted",
"testReserveSomeAttrWhenReplaceNode",
"testPreviousAll" ]
suite = unittest.TestSuite(map(TestSetAttr, test_names))
因为 TestSetAttr 是 callable,所以 map(function, seq1, [seq2, ...]) 会将它作为一个函数调用,并将 test_names 依次作为其调用时的参数。这样就会依次构建所有的 tests instance。

回到这个测试的具体案例上来。如果采用 unittest.main() 或 unittest.TestLoader().loadTestsFromModules(__main__) 的做法,有一个问题就是 tests 不会按照你定义他们的时候的上下顺序来运行(而且即使按照定义的顺序来运行也没有用,下面谈到),但是在定义测试的时候,如最开始所描述的,必须要考虑到对 Previous 条件进行测试的需要。如果我不做 testPreviousAll(),那么这样运行没有什么问题,但问题就在于做 testPreviousAll() 是有必要的。

一开始,使用 unittest.main() 或 unittest.TestLoader().loadTestsFromModules(__main__) 这种方法,在 TestTree 这个基类中定义的方法如下:
def _setAll(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
method_name = member[0]
if method_name.startswith("_set"):
names.append(method_name)
names.remove("_setAll")
for name in names:
method = getattr(self, name)
method()

def testPreviousAll(self):
self._setToggle = 0
self._setAll()
names = []
for member in inspect.getmembers(self, inspect.ismethod):
test_name = member[0]
if test_name.startswith("test"):
names.append(test_name)
names.remove("testPreviousAll")
# #1:
suite = unittest.TestSuite(map(self.__class__, names))
suite.run(suite)
# unittest.TextTestRunner(verbosity=2).run(suite)
# #2:
# for name in names:
# test_method = getattr(self, name)
# test_method()
然后运行:
 python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testPreviousAll (__main__.TestKeyIndex) ... ERROR
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testPreviousAll (__main__.TestSetAttr) ... ERROR
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok
testPreviousAll (__main__.TestTree) ... ERROR

======================================================================
ERROR: testPreviousAll (__main__.TestKeyIndex)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 113, in testPreviousAll
suite.run(suite)
File "/usr/lib/python2.4/unittest.py", line 422, in run
if result.shouldStop:
AttributeError: 'TestSuite' object has no attribute 'shouldStop'

======================================================================
ERROR: testPreviousAll (__main__.TestSetAttr)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 113, in testPreviousAll
suite.run(suite)
File "/usr/lib/python2.4/unittest.py", line 422, in run
if result.shouldStop:
AttributeError: 'TestSuite' object has no attribute 'shouldStop'

======================================================================
ERROR: testPreviousAll (__main__.TestTree)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 113, in testPreviousAll
suite.run(suite)
File "/usr/lib/python2.4/unittest.py", line 422, in run
if result.shouldStop:
AttributeError: 'TestSuite' object has no attribute 'shouldStop'

----------------------------------------------------------------------
Ran 19 tests in 0.462s

FAILED (errors=3)
显然,你不能向 suite.run() 传递 suite 作为参数,你只能向 suite.run() 传递 TestResult 的 instance 作为参数,或者将 suite 作为参数传递给 TestRunner 的 instance,亦即在上面的定义中使用的另外一种方法:unittest.TextTestRunner(verbosity=2).run(suite)。但这样运行会导致你的输出很不规整,你可能会得到类似这样的结果:
sh# python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testPreviousAll (__main__.TestKeyIndex) ... testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testUnhashable (__main__.TestKeyIndex) ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.157s

OK
ok
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testPreviousAll (__main__.TestSetAttr) ... testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.124s

OK
ok
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok
testPreviousAll (__main__.TestTree) ... testBaseCase (__main__.TestTree) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.006s

OK
ok

----------------------------------------------------------------------
Ran 19 tests in 0.759s
这样显然是不利于测试的组织和代码的排错的。所以改用在上面的定义中的 #2 所指明的办法。

在上面 testPreviousAll() 的定义中调用了 self._setAll(),这看上去会降低正交性,但是不再其中定义却不行,你会得到这样的错误:

# python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testPreviousAll (__main__.TestKeyIndex) ... ERROR
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testPreviousAll (__main__.TestSetAttr) ... ERROR
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok
testPreviousAll (__main__.TestTree) ... ERROR

======================================================================
ERROR: testPreviousAll (__main__.TestKeyIndex)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 118, in testPreviousAll
test_method()
File "test_tree.bk", line 360, in testDeepIndexedNodes
self.assertNodeSet(self.root.deep, "deep_indexed_prefix")
AttributeError: Tree instance has no attribute 'deep'

======================================================================
ERROR: testPreviousAll (__main__.TestSetAttr)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 118, in testPreviousAll
test_method()
File "test_tree.bk", line 168, in testAssignNonTreeToExisted
self.assertNodeSet(self.root.nt_ex_simple, "one")
AttributeError: Tree instance has no attribute 'nt_ex_simple'

======================================================================
ERROR: testPreviousAll (__main__.TestTree)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 118, in testPreviousAll
test_method()
File "test_tree.bk", line 84, in testBaseCase
self.assertNodeSet(self.root, "root")
AttributeError: 'TestTree' object has no attribute 'root'

----------------------------------------------------------------------
Ran 19 tests in 0.304s

FAILED (errors=3)
因为通过构造 TestSuite 的方法可以看出,每一次传递给它的构造函数的都是一个全新的 TestCase instance,并且每一个这样的 instance 中在运行的时候只有其中的一个 test* 方法被运行,就是 testMothod 所指向的那个方法。所以如果不先调用这个方法,那么任何节点都不会设置。

如果使用方法 #1,则似乎不会出现这个问题,所有测试都 OK 通过,但其实是不对的。因为在检查 PreviousAll 的时候,我必须避免对要检查的节点重复设置,所以在 test* 方法中使用了 _setToggle 标志来实现这一点。但因为使用 TestSuite() 创建了新的 tests,而这些 tests 的 _setToggle 必然是重新设置为 1 的,所以所有节点都会被重复设置,因此测出的结果是不正确的。如果将 _setToggle 定义为 class 的 static 变量呢?似乎可行,但显然会降低正交性,并且如前所述,测试的结果输出很不规范。

这也就是前面提到,即使按照顺序来调用这些 tests 也没有用的原因。如果要按照顺序来调用,可以这样:
fixture = unittest.TestSuite()
test_names = ["testBaseCase"]
suite = unittest.TestSuite(map(TestTree, test_names))
fixture.addTest(suite)
test_names = [
"testNameReserved",
"testCreateNew",
"testAssignNonTreeToExisted",
"testAssignTreeToExisted",
"testReserveSomeAttrWhenReplaceNode",
"testPreviousAll" ]
suite = unittest.TestSuite(map(TestSetAttr, test_names))
fixture.addTest(suite)
test_names = [
"testNodeWithoutKey",
"testNodeWithKeys",
"testInexistentNode",
"testOnlyValueReplacement",
"testNodeReplacement",
"testIndexedParentReplacement",
"testUnhashable",
"testDeepIndexedNodes",
"testPreviousAll" ]
suite = unittest.TestSuite(map(TestKeyIndex, test_names))
fixture.addTest(suite)
unittest.TextTestRunner(verbosity=3).run(fixture)
当然这样意义不大,而且显然增加了重复性。

即使是在 testPreviousAll() 中调用了 _setAll() 也仍然是有问题的,因为
for member in inspect.getmembers(self, inspect.ismethod)
这段代码也不会按照你定义的顺序去 inspect member methods,所以 _setBaseCase() 可能会在中间运行再次被运行(setUp() 里面运行过第一次,否则没有根节点,我这里实际上就在中间重新运行的),因此导致 root 节点被重新设置,结果是已经设置的其他节点被冲掉了,因此后面必然会抛出 AttributeError 异常。当然结果是不确定的,可能会出现这样的问题,也可能会巧合的按照定义顺序进行。

显然第一种思路是行不通了。那么第二种思路呢?
fixture = unittest.TestSuite()
suite = unittest.TestLoader().loadTestsFromTestCase(TestTree)
fixture.addTest(suite)
suite = unittest.TestLoader().loadTestsFromTestCase(TestSetAttr)
fixture.addTest(suite)
TestSetAttr._setToggle = 0
suite = unittest.TestLoader().loadTestsFromTestCase(TestSetAttr)
fixture.addTest(suite)
suite = unittest.TestLoader().loadTestsFromTestCase(TestKeyIndex)
fixture.addTest(suite)
TestKeyIndex._setToggle = 0
suite = unittest.TestLoader().loadTestsFromTestCase(TestKeyIndex)
fixture.addTest(suite)
unittest.TextTestRunner(verbosity=2).run(fixture)
这时候,不定义 testPrevious() 和 _setAll() 方法,并且 _setToggle 定义为类的 static 变量。结果当然仍然是不行,可以推想,因为所有的 tests 都是全新的 TestCase instance,相互之间没有关联,因此第二次产生的 suite 并不会使用第一次产生的 suite 的结果。在将 TestSetAttr._setToggle 设置为 0 后,调用 unittest.TestLoader().loadTestsFromTestCase(TestSetAttr) 产生的 tests,其 test* 将不再设置 _set* 方法,因此所有的节点都不会被设置,必然会抛出 AttributeError。实际运行的结果也是如此。

那么如何解决这个正交性测试独立性的问题呢?

因为同时必须要考虑到顺序的问题,但同时因为各个测试方法之间都是互相独立的,所以我接下来的一个思路就是在每一个 test* 方法中调用定义的前一个 test* 和 _set*。代码大概是这样(此时不再需要定义 testPreviousAll() 和 _setAll()):
class TestTree(unittest.TestCase):
...
def _testPrevious(self, test_name):
self._setToggle = 0
test_method = getattr(self, test_name)
test_method()
self._setToggle = 1

class TestSetAttr(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()
...
def _setCreateNew(self):
self._setBaseCase()
self.root.trunk = 1
self.root.branch = Tree(None, data='branch/data', extra=('branch', 'extra'))

def testCreateNew(self):
# """If the sub node does not exist, assign it directly"""
if self._setToggle:
self._setCreateNew()
self.assertNodeSet(self.root.trunk, 1)
self.assertNodeSet(self.root.branch, None)
self.assertNodeSet(self.root.branch.data, "branch/data")
self.assertNodeSet(self.root.branch.extra, ('branch', 'extra'))
self._testPrevious("testBaseCase")
这样在运行 testCreateNew() 的时候,会接着运行 testBaseCase() 以确保前面设置的节点没有受到 CreateNew 操作的影响,并且因为关闭了 _setToggle,不会导致重复设置。

但如果把这个问题扩展一下呢?就是说,如果我按照任意的顺序去调用 _set* 方法,那么能否始终保证后面的操作不会对前面的已有结果造成破坏呢?

另外还有一个问题。在:
python assemble methods at runtime?

python unzip
中,曾提到运行时装配方法的技巧。但在这里似乎行不通,因为这时候这个 testPreviousAll() 方法好像根本就没有被检测到(使用 loadTestsFromModules(__main__)):
sh# python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok

----------------------------------------------------------------------
Ran 16 tests in 0.280s

OK
看一下 unittest 的源代码,其调用关系是这样的:
loadTestsFromModules()
\--> loadTestsFromTestCase() # TestSetAttr
\--> getTestCaseNames(self, testCaseClass)

class TestLoader:
......
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
if issubclass(testCaseClass, TestSuite):
raise TypeError("Test cases should not be derived from TestSuite. Maybe you meant to derive from TestCase?")
testCaseNames = self.getTestCaseNames(testCaseClass)
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
testCaseNames = ['runTest']
return self.suiteClass(map(testCaseClass, testCaseNames))

def loadTestsFromModule(self, module):
"""Return a suite of all tests cases contained in the given module"""
tests = []
for name in dir(module):
obj = getattr(module, name)
if (isinstance(obj, (type, types.ClassType)) and
issubclass(obj, TestCase)):
tests.append(self.loadTestsFromTestCase(obj))
return self.suiteClass(tests)

def getTestCaseNames(self, testCaseClass):
"""Return a sorted sequence of method names found within testCaseClass
"""
def isTestMethod(attrname, testCaseClass=testCaseClass, prefix=self.testMethodPrefix):
return attrname.startswith(prefix) and callable(getattr(testCaseClass, attrname))
testFnNames = filter(isTestMethod, dir(testCaseClass))
for baseclass in testCaseClass.__bases__:
for testFnName in self.getTestCaseNames(baseclass):
if testFnName not in testFnNames: # handle overridden methods
testFnNames.append(testFnName)
if self.sortTestMethodsUsing:
testFnNames.sort(self.sortTestMethodsUsing)
return testFnNames
filter 是 builtin 函数,显然在使用 dir(testCaseClass) 的时候是不会包含 testPreviousAll() 的,因为 testPreviousAll()只会在 TestCase instance 中存在。

所以应该不是在 setUp() 方法里面设置,而是应该定义 TestTree.testPreviousAll = testPreviousAll。

包含这些问题的代码在 caxes/trunk/test/test_tree.bk 中:
__package__ = "caxes"
__revision__ = [257:259]

星期三, 十一月 07, 2007

sourceforge vhost

绑定 VirtualHost。在 Project -> Admin -> Shell/DB/Web -> "Manage VHOSTs" 下,添加一个 vhost(Add New Virtual Host -> New Virtual Host),例如我原来的项目站点为 crablfs.sourceforge.net,这时增加一个 www.yourlfs.net。

然后设置 DNS。注册一个域名 yourlfs.net(¥50/年),并进行如下设置:
yourlfs.net      IN    A        66.35.250.210
www.yourlfs.net IN CNAME vhost.sourceforge.net.
svn.yourlfs.net IN CNAME crablfs.svn.sourceforge.net.

dig DNS 迭代查询以及双线 view 问题

默认情况下,DNS 都会使用递归查询。通过迭代查询,可以获得查询的路径,即对域名是如何被解析的得到一个直观印象。可以通过运行
sh$ dig +trace www.example.com (@server)
来进行查询。

我在 "变态"DNS中曾经讨论过为双线设置 DNS 的方法。但最近发现老是会被解析到网通的服务器,即使是电信的 DNS 服务器,也会得到网通的结果。用 dig +trace 也看不出所以然来。

后来发现网通的 slave DNS 的两个域文件(电信 .zone 和 网通 .cnc_zone)的内容完全一样。原来从服务器同步的时候,因为地址是网通的,所以主服务器只会返回网通的结果。这样看来,从主服务器之间也是通过 53 端口传递数据并且也受 acl view 规则的影响。

要解决这个问题,复杂一点的办法是从服务器绑定两个 IP 地址,主服务器的两个 view 设置不同的 allow-transfer {},从服务器的两个 view 设置两个 transfer-source $ipaddr,可参考:
http://www.chinalinuxpub.com/read.php?wid=1452

简单点的办法就是把两个都设置成主的 DNS 并手工同步。

python setup.py classifiers for pypi

通过编写 setup.py 中的 classfiers,以及相应的 url/download_url 和 description,可以直接从命令行上运行 python setup.py register 将包直接上传到 pypi,从而避免每次发布新版本的时候都要登录到 web 页面填写表单这样的重复劳动。例子如下:
#!/usr/bin/python
# -*- encoding: utf-8 -*-

# Author: Roc Zhou
# Date: 2007-11-07
# Email: chowroc.z@gmail.com

from distutils.core import setup
from distutils import sysconfig

lib_prefix = sysconfig.get_python_lib()

setup(
name = 'caxes',
version = '0.1.4'
description = """
Some new Python data structure,
can be afforded as APIs for new ways of configuration.

It's a subproject of uLFS.

uLFS means "Your Manageable, Automatable and Templated Reusable Linux From Scratch",
it's a set of tools to build your own customed Linux distribution with
more managability than raw LFS(Linux From Scratch). Include source package
manager, file system backup and realtime mirror apps, and some assistant data
structure such as Tree writen in Python, etc...
""",
author = "Roc Zhou",
author_email = 'chowroc.z@gmail.com',
platforms = "Platform Independent",
license = "GPL v2.0",
url = "http://crablfs.sourceforge.net",
download_url = "http://sourceforge.net/projects/crablfs",
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License (GPL)",
"Natural Language :: English",
"Natural Language :: Chinese (Simplified)",
"Operating System :: POSIX",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Topic :: Software Development :: Libraries :: Python Modules"
],
py_modules = ['tree'],
# data_files = [('test', ['tree_ut.py'])],
data_files = [("%s/test" % lib_prefix, ['test_tree.py'])],
# package_dir = {'caxes' : 'lib'},
# packages = ['caxes']
)
不过不知道为什么 description 却和 web 页面中的 Description 不一样,却显示在了 Summary 下面。结果 Summary 信息很长。

星期日, 十一月 04, 2007

python distutils data_files lib_prefix

使用 distutils 的 setup() 的 data_files 参数,本来期望文件被拷贝到 /usr/lib/python2.4/site-packages/test 下面,结果却被拷贝到了 /usr/test(如果使用 upm 安装则没有权限),因为 prefix 为 /usr。

那么我希望改变这个设置。因为 /usr/lib/python2.4/site-packages 应该是在安装时的 LIB 目录设定下的,所以找到 lib_prefix 即可:
from distutils import sysconfig

lib_prefix = sysconfig.get_python_lib()

setup(
...
data_files = [("%s/test" % lib_prefix, ['tree_ut.py'])],
...

星期五, 十一月 02, 2007

SNMP access 的一个问题

SNMP Agent 的配置文件:
sh$ egrep -v '(^#|^$)' snmpd.conf
syslocation Unknown (edit /etc/snmp/snmpd.conf)
syscontact Root (configure /etc/snmp/snmp.local.conf)
pass .1.3.6.1.4.1.4413.4.1 /usr/bin/ucd5820stat
syscontact monitor@zovatech.com
disk / 36G
com2sec mynet 192.168.0.0/24 public
com2sec mynet 222.66.231.106 public
group mynet v1 mynet
group mynet v2c mynet
view system included .1
access mynet "" any noauth exact system none none
exec .1.3.6.1.4.1.2021.54 hdNum /usr/local/bin/snmpdiskio hdNum
exec .1.3.6.1.4.1.2021.55 hdIndex /usr/local/bin/snmpdiskio hdIndex
exec .1.3.6.1.4.1.2021.56 hdDescr /usr/local/bin/snmpdiskio hdDescr
exec .1.3.6.1.4.1.2021.57 hdInBlocks /usr/local/bin/snmpdiskio hdInBlocks
exec .1.3.6.1.4.1.2021.58 hdOutBlocks /usr/local/bin/snmpdiskio hdOutBlocks
本来没有设置 com2sec mynet 222.66.231.106 的。因为 SNMP client 这台主机有两个 IP,一个是内网的 192.168.0.1,还有一个是外网的 202.66.231.106,而 SNMP Agent 所在主机为 192.168.0.197。则在配置 SNMP access 的候,一个很直接的想法就是只要允许了所有内网的主机就应该可以了呀,但是在这里却不行,对于 192.168.0.1 这台主机,必须要使用外网(eth1)的那个 IP 来指定。

不知道原因是什么?

星期四, 十一月 01, 2007

项目自动化

《Pragmatic Project Automation》主要是针对 Java 项目的自动化建构和部署以及监控。我目前主要使用 Python,接着需要重新再学习 C,那么我需要考虑一些不同的情况。

首先,我不大可能使用象 Ant 和 CruiseControl 这样的工具,不过我想应该可以直接利用 Python 的 distutils 工具基本上也可以做这些。

首先,为了使测试能够自动化,并且编写单元测试的时候能够更加一致,也许应该调整一下目录结构。以 caxes 项目为例,目前的目录结构是:
ulfs/
caxes/
tree.py
test_tree.py
lib/
ctemplates.py
edconfig.py
sctmd.py
test/
test_ctemplates.py
test_edconfig.py
test_sctmd.py
......
但对于单元测试文件的存放就不一致了。另一个问题是,test_ctemplates.py 要 import ctemplates.py,在 sandbox 中和安装后的包路径会不一致,因为安装后显然只能是 caxes 包(或其他名字如 libcaxes,但不可能是 lib),虽然可以象这样:
try:
pwd = os.getcwd()
MODULE_PATH = os.path.dirname(pwd)
sys.path.insert(0, MODULE_PATH)
import mirrord,fs_info
sys.path.pop(0)
except ImportError:
from cutils import mirrord,fs_info
但这会导致重复增加。所以我想这样的目录结构也许更合理:
ulfs/
caxes/
lib/
tree.py
caxes/
ctemplates.py
edconfig.py
sctmd.py
test/
test_tree.py
caxes/
test_ctemplates.py
test_edconfig.py
test_sctmd.py
......
然后做一个构建脚本,在其中首先设置 PYTHONPATH,并调用 setup.py build --build-base=build/,并进入 build/ 目录自动运行所有的测试即可。例如:
#!/bin/sh
cd /opt/automated/
svn co http://crablfs.svn.sourceforge.net/svnroot/crablfs ulfs/
cd ulfs/caxes/
python setup.py build --build-base=build/
cd build/
export PYTHONPATH=`pwd`/lib
python test/test_ctemplates.py
python test/test_edconfig.py
python test/test_sctmd.py
...
当然应该能够写的更好?不过我不知道有没有 Python 下对应的工具?

另一个问题是如果将定时构建脚本的输出重定向到邮件、短信或信号灯,这应该属于监控的问题了。

另外,书中写到在安装和部署时使用诊断测试排除故障的技巧是比较有价值。