事情的起因是这样子的,小员在写一个测试工具,主要是管理 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)

No related posts.

Tags:



Leave a Comment