AHC ローカルでテストケースを自動実行する環境構築メモ

記事の内容

AHCのテストケース複数個を自動で処理してくれる環境をローカルで構築する方法。

※2023/01/08追記
下記リンクの方で既に同様のことが行われていました。

※2023/03/18午前追記
対話型問題のローカル環境実行方法について、メモ程度ですが書きました。

動機

記事を書こうと思った動機は主に以下の3点です。

  • AHCのテストケースをローカル上において、自動で実行する環境が欲しかった

  • ビジュアライザをローカルでいい感じに動かすページがパッと見当たらなかった。

  • noteで何か書いてみたかった

AHCとは

AHCとは、AtCoder社が提供するプログラミングのコンテストの一種で、最適な解を求めることが困難な問題に対し、可能な限り良い解を出力するコードを作成するコンテストである。

想定読者

想定する読者は、

  • AHCを始めたい、あるいは始めているがローカル環境を作っていない人

  • ローカル環境において自動でテストケースを実行する環境を作りたい人

  • C++でコードを書いてる

です(想定読者の母数かなり少なそう)。テストケース実行の自動化にはPythonを使っているので、Pythonのコードを読める&メンテが出来ると後から修正加筆がしやすいと思います。

準備

環境

OSはWindows10で、AHC提出用のコードはC++で書きました。

必要なもの

  • gcc(C++をビルドする)

  • Python実行環境

  • Rust実行環境

  • AtCoder公式配布のRustビジュアライザ(公式問題ページにて配布されている)

  • 実行したいC++のコード

上記必要な物の入手先について、gccのインストールは若干躓くことがあるかも(参照: MinGWを使ってgccインストール)。

テストケース実行の流れ

テストケース実行の大まかな流れは次の通りです。

  1. AtCoderの問題ページからビジュアライザのzipファイルをダウンロードして解凍

    1. toolsというフォルダが生成される

    2. 入力ファイルはinフォルダの中に格納されている

  1. もし入力用ファイルが無ければ生成(作成方法はビジュアライザのReadme.htmlに記載されている)

  2. toolsフォルダに、実行したいC++コードをmain.cppという名前で保存

  3. run.py(コードは後述)というPythonファイルをtoolsフォルダに保存し実行

実行がうまくいけばoutputフォルダにC++の実行結果が記録され、resultフォルダに結果がまとめられます。

実行するPythonファイル

実行するPythonコードは、以下のソースです。私はPythonが得意ではないのでChatGPT君に色々聞きながら作りました(簡単な要求だったらChatGPTでも多分正しい出力をしてくれるでしょう)。
ソースコードはrun.pyという名称でtoolsフォルダ上に保存してください(実行は自己責任でお願いします)。

##run.py##
import os
import subprocess
import shutil
import time
import multiprocessing

#並列処理するときのプロセス数
num_processes = 1

#ビルドするC++ファイル名
cpp_file_name = "main.cpp"
#C++の実行ファイル名(OSによって違う?)
executable_file_name = "./a.exe"
#出力結果を記入するファイル名
output_file = "result/result.txt"

def init():        
    #出力結果を格納するフォルダ作成
    if not os.path.exists("out"):
        os.makedirs("out")
    if not os.path.exists("result"):
        os.makedirs("result")
    if not os.path.exists("result/visualizer"):
        os.makedirs("result/visualizer")
    # main.cppをビルドする
    subprocess.run(["g++", cpp_file_name])
    
    #既存結果の消去
    with open(output_file,"w")as f:
        f.write("")
    return

#inフォルダ内にある.txtファイルのリストを返す
def get_txt_list():   
     #inフォルダにある入力用の.txtファイルをすべて読み込む
    files = os.listdir("in")
    txt_files = [f for f in files if f.endswith(".txt")]
    return txt_files


#入力がfile_nameであるファイルに対するC++実行ファイルを実行 返り値: 実行時間
def exec_cpp(file_name):    
    run_time = 0.0
    #inフォルダにあるtxtファイルの入力に対し、コードを実行する。出力結果はoutフォルダにinフォルダのファイル名と同じ名称で保存
    with open("in/" + file_name, "r") as f_in:
        with open("out/" + file_name, "w") as f_out:
            start_time=time.perf_counter()
            subprocess.run([executable_file_name], stdin=f_in, stdout=f_out)#生成される実行ファイル名(OSによって違う?)
            end_time=time.perf_counter()
            run_time=end_time-start_time
    return run_time

#入力がfile_nameであるテストケースの結果を出力
def output(file_name, run_time):
    with open(output_file,"a")as f_out:
        #TestCaseの名称を出力
        f_out.write("\nTest Case: "+str(file_name)+"\n")
        f_out.write("Run Time is: "+str(run_time)+"[s]\n")
        #TestCaseのテキストファイル1行目を出力(入力データのサイズであることが多いため)(コンテストの入力に応じて変えること)
        with open("in/"+file_name,"r")as f_in:
            line = f_in.readline()
            f_out.write(line)

    #公式配布のRustを実行&結果の書き込み
    with open(output_file,"a")as f_out:
        subprocess.run(["cargo", "run", "--release", "--bin", "vis", "in/" + file_name, "out/" + file_name], stdout=f_out)
    #ビジュアライズ結果をコピーして移動する処理
    if os.path.exists("vis.html"):
        shutil.copy("vis.html","result/visualizer/vis"+file_name+".html")
    return    

#入力がfile_nameであるテストケースを実行し、結果をresult.txtに出力してビジュアライズ結果を保存する
def run_test_case(file_name):
    run_time = exec_cpp(file_name)
    output(file_name,run_time)
    return

def main():
    init()
    txt_files = get_txt_list()#入力ファイルリストの獲得
    with multiprocessing.Pool(processes=num_processes)as p:
        p.map(run_test_case,txt_files)
    return

if __name__ == "__main__":
    main()

run.pyの処理内容

run.pyの処理内容は主に次のようになります。

  1. main.cppのビルド

  2. 結果をまとめたテキストファイルがあれば中身を消去

  3. inフォルダ内にある入力ファイルを標準入力として、ビルドしたC++実行ファイルを実行し、標準出力をoutフォルダに格納

    • この際、実行時間も記録

  4. AtCoder公式配布のRustを実行

    • 標準出力にスコアが出力され、さらにビジュアライズ結果が得られる

  5. 諸々の記録をresultフォルダに格納

    • 記録する内容:

      • 実行した入力ファイル名

      • C++実行ファイルの実行時間

      • Rustの標準出力(Scoreである場合が多い)

      • 入力ファイルの1行目(データサイズが書かれていることが多いため)

  6. 3~5の流れをinフォルダにある.txtファイルすべてに対して1回ずつ行う

実行時間短縮のため、並列処理にも対応させています。
もし並列処理させたい場合は、上記ファイルにあるnum_processesの値を実行したいプロセス数に変えてください。
ただし、並列処理が原因で出力される結果に悪影響が出る可能性があることはご了承ください(実際に発生を確認したわけではないが一応)。

もし、C++以外の言語で書いたコードを実行したい場合、
init関数の中のmain.cppのビルド部分と、exec_cpp関数の内部を変更してください。どう変更すればよいかはChatGPT君がいい感じに答えてくれると思います(メジャーな言語であれば)。

使用例

AHC13を例にします。
まず、公式の問題ページの下の方にあるツールの章にある「ローカル版ビジュアライザ・入力ジェネレータ」をダウンロードする。

AHC13の問題のページ

その後、ダウンロードしたzipファイルを解凍する。

toolsフォルダはデフォルトでこのような構成になっている

run.pyと実行したいC++ファイル(main.cppという名称にすること)をtoolsフォルダ内に入れる。

run.pyを実行する。うまくいったら下記のような画面が出てくる。

コマンドプロンプトで実行した場合

実行結果はresultフォルダ出力される。
複数プロセスで実行した場合、出力結果は必ずしも昇順に並んでいない(プロセス終了順に記録されるため)。
また、何も書かれていない標準出力ファイルがあった場合、C++側の方でエラーが発生した可能性が高いです。

複数プロセスで実行した例

問題が対話型の場合におけるローカル環境のテスト実行方法

2023年3月18日午前追記(まだAHC019は始まっていないのでセーフ!)
AHC018のような対話型問題で、ローカル環境においてテストケースを実行する方法がわからず少し躓いたのでメモ程度に書いておきます。この方法が正攻法なのかはわかりません。悪しからず。

AHC018のような対話型問題をローカルで実行するには、本来サーバー側が行う処理をmain.cpp内で実行することにより解決しました。
AHC018を例に挙げると、ローカルテスタの入力ファイルに、本来は付与されない岩盤の強度の情報が記載されています。そのため、ローカルでテストケースを実行したいときは、各岩盤の強度情報を読み取ってサーバーサイドの処理を別途記述することでローカルテスタを正常に動かすことが出来ました。
私の場合、boolでテスト用か提出用か切り替え可能にしておき、テスト用の場合、本来では掘削の標準出力する時にmain.cpp内部で用意したサーバーの肩代わりをする構造体に掘削情報を投げ、本来は標準入力で得る結果情報rを算出という感じで乗り切りました。


既知の問題

今回書いた自動化処理では、既知の問題が複数あります。

  • 並列処理による結果への悪影響

    • 結果を書き込むファイル、生成されるビジュアライザは単一であるため、記録やビジュアライザのコピータイミングによっては想定と異なるものが得られる可能性があるかも

  • 対話型形式の問題には未対応

    • 上記のコードは、標準入力を1度与えたら標準出力が得られる形式のみに対応

    • 今後そのような問題が出てきた場合、そのときまた修正するかも

  • Rustの実行形式が通常と異なる形式に未対応

    • 例えばAHC16には対応できていない

  • C++でエラーが発生した場合の処理が未対応

    • python自体も落ちるということは無いが、どの部分でエラーが発生したか出力されない

  • データ解析用には出来ていない

    • 平均点とかは出力されないので、平均とか欲しい場合はコードを書きなおしてください

まとめ

ローカル環境でテストケースを自動実行する環境の構築手順をメモがてら書きました。
誰かの役に立ったら嬉しいです。
何かご指摘がありましたらコメント等よろしくお願いいたします。

今後の展望

プロの人たちはAWSとかGoogleCloudを使って実行環境を構築しているようです。そもそもAtCoderもAWSを使って実行しているようなので、実行時間を擦り合わせるという意味でもAWSなどのクラウド上で実行する方が賢明かと思います。私はAWSの環境構築に挑戦しましたが難しくてうまく出来ませんでした。いつかクラウド上で実行する環境を作りたいです。

以下、AHC用にクラウド上で環境を構築する記事のリンク