競プロでローカルテストをするツールを作った

競プロを勉強し始めたのだが,ローカルテストがめんどくさい.
毎度,入力例を入力し,出力例と照合するのがめんどくさい.

ITの最先端の遊びをしているはずなのに,
自分はなぜこんな面倒なことをしているんだと思い,ローカルテスターを作った.

作っている途中に気づいたが,もうすでにいっぱいそのようなツールがあった.
中でも,online-judge-toolsというのが便利そうであることを知った.
https://github.com/kmyk/online-judge-tools
https://kimiyuki.net/blog/2017/01/19/pr-online-judge-tools/

まぁ車輪の再発明でもいいじゃない.
自分はこうして学んでいくんだ!と思い,こそこそ作ったので,作り方を示すことにした.

コードはこちらにある.
https://gist.github.com/gusugusu1018/1a0948c2254691f6996cf1f2b3f31662

作る上で重要視したポイントは3つある.

  1. そのままアップロードできるようにする.
  2. 実行時間を知りたい.
  3. コンパイル後自動で走らせるようにする.

1つ目のポイントを抑えるには,標準入力を自動で行うのがいいと考えた.
そこで使用したのがヒアドキュメントである.

実行時に標準入力を行う方法

これは,ヒアドキュメントを活用するだけである.

次のようにヒアドキュメントを使用すると,test.outという実行ファイルを実行時に自動的にEOFからEOFまでのあたいの標準入力が行われる.

1
2
3
4
5
6
./test.out <<EOF
5
2
4 2 3 4 5
2 5 3 1 4
EOF

これを使用することで,煩わしい入力を手打ちする必要はなくなる.

pythonで外部コマンドを使用する

今回ツールを作るのにあたり,最初はシェルスクリプトを使っていたのだが,
機能を盛り込むにつれ,pyhtonの方が書きやすい事に気付きpythonで外部コマンドを使用することにした.

python3では,subprocessを使って様々な方法で外部コマンドを走らせることができる.
python 3.5 後では,subprocess.run()やsubprocess.Popen()を使用できるが,今回はpython3.5以前にも対応しているAPIのsubprocess.call()とsubprocess.check_output()使った例を示す.

APIの詳しい仕様はこちらに載っているので割愛する.
https://docs.python.org/ja/3/library/subprocess.html

外部コマンドを使ってpythonでプログラムをg++コンパイルする

以下のようにC++プログラムをコンパイルできる.

1
2
3
4
5
6
import subprocess
import os.path
base, ext = os.path.splitext('test.cpp')
exe = base + '.out'
compile_cmd=['g++',args.src,'-std=c++17','-o',exe]
res = subprocess.call(compile_cmd)

ただし,subprocess.callした場合の返り値は,プロセスが正常に終了したのかどうかしか読み取ることができない.

外部コマンドを使ってpythonでC++プログラムを実行する

以下のようにシェルを実行できる.

1
2
3
4
import subprocess
test_cmd = './test.out <<EOF¥n5¥n2¥n4 2 3 4 5¥n2 5 3 1 4¥nEOF'
ret = subprocess.check_output(test_cmd,shell=True)
print(ret.decode('utf-8'))

subprocess.check_outputでは,プログラムの標準出力を受け取ることができる.

テストファイルフォーマットを決定する

[input]の後に入力,[output]の後に出力をコピペすることにした.

例えば,ABC081Aの問題のテストファイルは以下のようになる.
https://atcoder.jp/contests/abs/tasks/abc081_a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[input]
111
[output]
3
[input]
101
[output]
2
[input]
100
[output]
1
[input]
001
[output]
1

正解か不正解かを判定する

testファイルを読み込み,ヒアドキュメントでプログラムの実行時に標準入力を入れる.
その後標準された値をcheck_outpu()で読み込み正解データと比較する.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
test_data = open('ABC081A.test', "r")
lines = test_data.readlines()
test_cmd = './test.out << EOS\n'
inout = True # True is input False is output
for line in lines:
if line == '[input]\n':
inout = True
print('------test------')
elif line == '[output]\n':
inout = False
test_cmd += 'EOS'
ret = subprocess.check_output(test_cmd,shell=True)
test_cmd = './test.out << EOS\n'
else:
if inout:
test_cmd += line
else:
print('Correct Answer : ' + line + 'Program output : ' + ret.decode('utf-8'))
if line == ret.decode('utf-8'):
print('Correct!')
else:
print('Wrong!')
test_data.close()

最終的なプログラム

コマンドラインパーサーを導入したり,実行時間を調べたり,正解率を出したりしてみた.

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
#!/usr/bin/env python3
import sys
import os.path
import subprocess
import time
import argparse

if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Local debug tool for competitive programing.')
parser.add_argument('src', help='Sourcecode file')
parser.add_argument('-t','--test',help='Test file. default file = src.test')
parser.add_argument('-d','--debug', action='store_true', help='set debug mode')
parser.add_argument('-v','--verbose', action='store_true', help='Activate print function')
args = parser.parse_args()

base, ext = os.path.splitext(args.src)
exe = base + '.out'
compile_cmd=['g++',args.src,'-std=c++14','-o',exe]
res = subprocess.call(compile_cmd)

if args.test == None:
test_file = base + '.test'
if os.path.exists(test_file):
args.test = test_file

if res==0:
print('Compile success')
if args.test != None:
test_data = open(args.test, "r")
lines = test_data.readlines()
test_cmd = './' + exe + '<< EOS\n'
test_counter = 0
correct_counter = 0
average_time = 0.0
inout = True # True is input False is output
for line in lines:
if line == '[input]\n':
inout = True
test_counter+=1
if args.verbose or args.debug:
print('------test' + str(test_counter)+'------')
elif line == '[output]\n':
inout = False
test_cmd += 'EOS'
if args.verbose or args.debug:
print(test_cmd)
start = time.time()
ret = subprocess.check_output(test_cmd,shell=True)
elapsed_time = time.time() - start
average_time += elapsed_time
test_cmd = './' + exe + '<< EOS\n'
else:
if inout:
test_cmd += line
else:
if args.verbose or args.debug:
print('Correct Answer : ' + line + 'Program output : ' + ret.decode('utf-8'))
if not args.debug :
if line == ret.decode('utf-8'):
if args.verbose:
print('Ding ding ding! Correct!')
correct_counter+=1
else:
if args.verbose:
print('Wrong!')
if args.verbose:
print('elapsed_time:{0}'.format(elapsed_time) + '[sec]')
test_data.close()
if not args.debug:
print('------Result------\n')
print('Accuracy rate : ' + str(correct_counter/test_counter*100) + '%')
print('Average time : ' + str(average_time/(test_counter)) + '[sec]')
else:
res = subprocess.check_output(compile_cmd)
print(res)

割と簡単にこんな感じのツールは作れたので満足である.

以上,徒然文を最後まで読んでいただきありがとうございました.
何かあればコメント欄にて気軽に質問,ご意見ください!

Author: Gusugusu
Link: https://gusugusu1018.github.io/2019/09/10/競プロでローカルテストをするツールを作った/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment