runtime-test

runtime-test

一般来说,我们的测试代码是测试自己写的代码,测试模块的功能,但如果我们想自动化测试一个.py文件或者exe程序,就完全不一样了
如何自己写一个简易的测评姬?就是实现自动读取样例和输出样例对比来确定程序是否正确

思路:启动一个子线程,将子线程的输入输出流重定向方便我们获取,然后样例标志答案保存至文件,读取即可。

需要用到Python的subprocess模块

subprocess模块

运行python的时候,我们都是在创建并运行一个进程。像Linux进程那样,一个进程可以fork一个子进程,并让这个子进程exec另外一个程序。在Python中,我们通过标准库中的subprocess包来fork一个子进程,并运行一个外部的程序。
subprocess包中定义有数个创建子进程的函数,这些函数分别以不同的方式创建子进程,所以我们可以根据需要来从中选取一个使用。另外subprocess还提供了一些管理标准流(standard stream)和管道(pipe)的工具,从而在进程间使用文本通信。

  • subprocess.call()
    父进程等待子进程完成
    返回退出信息(return code,相当于Linux exit code)

  • subprocess.check_call()
    父进程等待子进程完成并返回0
    检查退出信息,如果returncode不为0,则举出错误subprocess.CalledProcessError,该对象包含有returncode属性,可用try…except…来检查

  • subprocess.Popen()
    启动一个子线程,它有一个复杂的构造函数

1
2
3
4
5
6
7
def __init__(self, args, bufsize=-1, executable=None,
stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS,
shell=False, cwd=None, env=None, universal_newlines=False,
startupinfo=None, creationflags=0,
restore_signals=True, start_new_session=False,
pass_fds=(), *, encoding=None, errors=None)

经常用到的参数也不多:

参数 解释
args 要执行的shell命令,可以是字符串,也可以是命令各个参数组成的序列。当该参数的值是一个字符串时,该命令的解释过程是与平台相关的,因此通常建议将args参数作为一个序列传递。
bufsize 指定缓存策略,0表示不缓冲,1表示行缓冲,其他大于1的数字表示缓冲区大小,负数 表示使用系统默认缓冲策略。
stdin,stdout 用于重定向标准的流。
shell 该参数用于标识是否使用shell作为要执行的程序,如果shell值为True,则建议将args参数作为一个字符串传递而不要作为一个序列传递。
universal_newlines 不同系统的换行符不同。若True,则该文件对象的stdin,stdout和stderr将会以文本流方式打开;否则以二进制流方式打开。

比较特殊的地方是,我们可以指定使用subprocess.PIPE
并可以利用subprocess.PIPE将多个子进程的输入和输出连接在一起,构成管道(pipe),这实际就是建立了一块缓冲区

Popen的方法:

函数 解释
Popen.poll() 用于检查子进程是否已经结束。设置并返回returncode属性。
Popen.wait() 等待子进程结束。设置并返回returncode属性。
Popen.send_signal(signal) 向子进程发送信号。
Popen.terminate() 停止(stop)子进程。在windows平台下,该方法将调用Windows API TerminateProcess()来结束子进程。
Popen.kill() 杀死子进程。
Popen.stdin() 如果在创建Popen对象时,参数stdin被设置为PIPE,Popen.stdin将返回一个文件对象用于向子进程发送指令。否则返回None。
Popen.stdout 类似上
Popen.pid 获取子进程的进程ID。
Popen.returncode 获取进程的返回值。如果进程还没有结束,返回None。
  • Popen.communicate()
    用于获取缓冲区的数据并且避免死锁情况:
    如果 stdout或 stderr 参数是 pipe,并且程序输出超过操作系统的 pipe size时,如果使用 Popen.wait() 方式等待程序结束获取返回值,会导致死锁,程序卡在 wait() 调用上。
    需要注意的是,communicate获取到的是一个元组,包含stdout和stderr
    同时communicate()是Popen对象的一个方法,该方法会阻塞父进程,直到子进程完成,所以不需要wait了

重定向标志流

sys.stdin = 文件流即可

获取文件的路径

使用sys.getcwd(),得到的就是你文件的所在文件夹路径

简单测评姬的代码

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
class VirtualTestGirl:
"""
测评姬, 仅供此次爱特国庆作业的测评使用
存在安全问题
"""
def __init__(self, stdin_file_list, code_file_path, q_number):
"""
文件的格式标准:
qx_x.txt存储样例
qx_x_out.txt 存储标准输出
:param stdin_file_list:标准输入的文件的文件名列表
:param code_file_path: 待测试的代码的文件的绝对路径
:param q_number: 题目的编号
"""
self.q_number = q_number
# 获取绝对路径
absolute_path = (os.getcwd() + '/{}/'.format(q_number)).replace('\\', '/')
self.stdin_file_list = [absolute_path + x for x in stdin_file_list]
self.code_file_path = code_file_path
self.error_file = "log.txt"

def _test(self, stdin_file):
"""
测试某个样例
:param stdin_file: 标准数据的文件名
:return:
"""
error_fp = open(self.error_file, 'a+', encoding='utf-8')
fp_in = open(stdin_file)
output_path = stdin_file.split('.')[0] + "_out.txt"

std_out_fp = open(output_path, encoding="utf-8")
std_out = std_out_fp.read()
std_out_fp.close()

error_fp.write("----测试----\n")
child_proc = subprocess.Popen("python {}".format(self.code_file_path), stdin=fp_in, stdout=subprocess.PIPE, stderr=error_fp)
print("pid:", child_proc.pid)
# 接受子线程的返回
print(child_proc.returncode)
# 获取输出
# 主线程等待子线程的完成, 此时会阻塞主线程
out_put = child_proc.communicate()[0].decode("utf-8")
# 删除文件读出内容结尾的\r\n
out_put = out_put.rstrip("\n")
out_put = out_put.rstrip("\r")
out_put = out_put.replace('\r', '')
# print("标准答案:", std_out)
# print("测试答案:", out_put)
if out_put == std_out:
print("通过!")
else:
if out_put.strip() == std_out.strip():
print("格式错误")
else:
pass
print("未通过")

fp_in.close()
error_fp.close()
print("------")

def start_test(self):
for stdin_file in self.stdin_file_list:
self._test(stdin_file)
  • 需要注意的是,不同平台下的换行符不同,MAC下是\r, Windows下默认是\r\n,Linux下是\n
  • 有神奇的一个地方是,print函数打印出来的是\n, 但定向输出到PIPE里然后再读出时变成了\r\n(可以split切一下\n看看会发现每行多了一个\r),推测为写入内存里的时候发生了替换,因为Windows下是\r\n
  • rstrip()会去除字符串末尾的指定字符,而strip()是删去字符串中所有匹配到的指定字符(默认删除\r\n)
Author

Ctwo

Posted on

2019-09-26

Updated on

2020-10-25

Licensed under

Comments