事情的起因是这样子的,小员在写一个测试工具,主要是管理 Xvfb 和 Selenium Server,启动多个 Server 来并行跑多个测试用例,出错的时候截图生成报告。
然后开发人员的测试代码是这么写的:

?View Code PYTHON
1
2
def setUP(self):
    self.selenium = selenium(host, port, "*firefox", "http://xxx.yyy.zzz:1234/")

这里的 host 和 port 在测试用例里是写死的,而在这个测试工具中,由于启动的 Selenium Server 的端口是随机绑定的,所以需要自动的把测试用例里的连接修改成对应的参数。
我俩讨论了下,决定用 decorator 来改掉 selenium 的构造函数,然后有了以下关键部分的代码:

?View Code PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def change_selenium_params(host, port, browser='', baseURL=''):
    def inner(func):
        def wrapper(*kargs, **kwargs):
            kargs = list(kargs)
            kargs[1:3] = host, port
            return func(*kargs, **kwargs)
        return wrapper
    return inner
 
@stop_servers
def run(self):
    """
    Run testcases
    """
    self.xvfb.start()
    self.selenium_server = serverctl.SeleniumServer(self.xvfb.display)
    self.selenium_server.start()
 
    for testcase_file in self.testcases_status['waiting']:
        selenium.selenium._init_ = change_selenium_params('localhost', self.selenium_server.port)(selenium.selenium.__init__)
 
        # Make testsuite and run it here
        ......
 
        self.selenium_server.restart()
 
    self.selenium_server.stop()
    self.xvfb.stop()

由于 Selenium 关闭 Firefox 有 Bug,所以不得不每跑一个 testsuite 就重启一次 Selenium Server,这时候发现 testsuite 用的还是第一次传过去的端口。结果看起来奇怪,这个问题还和其它混在一起了,而且这玩意比较难跟踪,调试器也不好用,于是乎,我们写了个简化版的原型来分析,代码如下:

?View Code PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Selenium:
    def __init__(self, port):
        print 'Port:', port
 
def change_port(port):
    def inner(func):
        def wrapper(*kargs, **kwargs):
            kargs = list(kargs)
            kargs[1] = port
            return func(*kargs, **kwargs)
        return wrapper
    return inner
 
for i in xrange(3):
    Selenium.__init__ = change_port(i)(Selenium.__init__)
    a = Selenium(9)

这时候的输出是:

1
2
3
Port: 0
Port: 0
Port: 0

然后在 decorator 里加了条输出语句:

?View Code PYTHON
1
2
3
4
5
6
7
8
9
def change_port(port):
    def inner(func):
        def wrapper(*kargs, **kwargs):
            kargs = list(kargs)
            print 'Change port:', port  # Add a debug output.
            kargs[1] = port
            return func(*kargs, **kwargs)
        return wrapper
    return inner

这时候的输出是:

1
2
3
4
5
6
7
8
9
Change port: 0
Port: 0
Change port: 1
Change port: 0
Port: 0
Change port: 2
Change port: 1
Change port: 0
Port: 0

这下终于知道问题所在了,可怜的 A.__init__ 函数每次执行都多被套一层 decorator,到了最后就变成了:

?View Code PYTHON
1
change_port(2)(change_port(1)(change_port(0)(A.__init__)))

嗯,用个临时变量先把初始的 A.__init__ 保存起来就 OK 了,比如:

?View Code PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A:
    def __init__(self, port):
        print 'Port:', port
 
orig_func = A.__init__
 
def change_port(port):
    def inner(func):
        def wrapper(*kargs, **kwargs):
            kargs = list(kargs)
            print 'Change port:', port  # Add a debug output.
            kargs[1] = port
            return func(*kargs, **kwargs)
        return wrapper
    return inner
 
for i in xrange(3):
    A.__init__ = orig_func
    A.__init__ = change_port(i)(A.__init__)
    a = A(9)

Tags:

Djangoat is short for Django Auto Tester, but I often pronounce it “djan-goat”… This script does monitor Django project directory by inotify mechanism, run unit tests when file changed, and notify errors through Mumbles if tests failed. It depends on inotify and Mumbles, so runs on Linux only, Mac version comes later…

Python code:

?Download djangoat.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import commands
import datetime
import dbus
import dbus.service
import os
import sys 
 
from dbus.mainloop.glib import DBusGMainLoop
 
from pyinotify import WatchManager, ThreadedNotifier, \
                      ProcessEvent, IN_CLOSE_WRITE, \
                      ExcludeFilter
 
FIREFOX_DBUS_INTERFACE = 'org.mozilla.firefox.DBus'
FIREFOX_DBUS_PATH = '/org/mozilla/firefox/DBus'
 
class FireFoxDBus(dbus.service.Object):
    def __init__(self, bus_name):
        dbus.service.Object.__init__(self, bus_name, FIREFOX_DBUS_PATH)
 
    @dbus.service.signal(dbus_interface=FIREFOX_DBUS_INTERFACE, signature='ss')
    def DownloadComplete(self, title, subject):
        pass
 
# Which type of files' change should be monitor
MONITOR_EXTENSIONS = ('.py', '.html')
 
class Watcher(ProcessEvent):
    def process_IN_CLOSE_WRITE(self, event):
        global cmd
        global firefox_dbus
 
        for extension in MONITOR_EXTENSIONS:
            if event.pathname.endswith(extension):
                start_time = datetime.datetime.now()
                print start_time
                output = commands.getoutput(cmd)
                print output
                # If test failed, call mumbles for notification
                if not output.endswith('OK'):
                    firefox_dbus.DownloadComplete(start_time.isoformat(), output)
 
    def process_default(self, event):
        pass
 
if __name__ == '__main__':
    if len(sys.argv) < 2:
        print 'Please specify a path for monitoring...'
        sys.exit()
 
    path = sys.argv[1]
    cmd = "python %s/manage.py test -v 0" % path
 
    # Exclude filter object
    excl_file = os.path.join(os.getcwd(), 'exclude.patterns')
    excl = ExcludeFilter({excl_file: ('excl_lst',)})
 
    # Add watch
    wm = WatchManager()
    notifier = ThreadedNotifier(wm, Watcher())
    wm.add_watch(path, IN_CLOSE_WRITE, rec=True, \
                 auto_add=True, exclude_filter=excl)
 
    # Set up an event loop
    dbus_loop = DBusGMainLoop()
    name = dbus.service.BusName(FIREFOX_DBUS_INTERFACE,
                                bus=dbus.SessionBus(mainloop=dbus_loop))
    firefox_dbus = FireFoxDBus(name)
 
    try:
        notifier.loop()
    except KeyboardInterrupt:
        print 'Djangoat shut down...'
    except Exception, ex:
        print 'Exception in Djangoat: %s' % (ex)
1
2
3
4
5
6
#  -*- mode: python; -*-
 
# Exclude pattens
excl_lst = ['^\.git/',
            '^\.svn/',
           ]

Screenshoot:
djangoat
Code repository:
http://git.lazytech.info/?p=daily-scripts.git

Tags: , , ,

AltSwitch chinese

Mozilla, Python November 7th, 2008

Download: AltSwitch 0.1
Description: A firefox extension which make windows version’s firefox use alt+(1-9) for switching tab instead of original ctrl binding

Tags: