AtCoder用の自動テスト環境を構築する

こんにちは。
水色コーダーのヤスローと申します。

みなさんAtCoderでコンテスト参加時に毎回テストケースを手動で実行してないですか？

今回、エディタでコードを保存するだけでテストケースを自動で実行できるツールを作りました。

# 自動実行環境用コードセット

より詳細には、**フォルダ内の「最も新しいpythonファイル」を探索し、一定間隔で実行し続けるプログラム**です。

以下が想定する実行環境です。`AutoTester`ディレクトリ内のファイルを作っていきます。
```
.
└── AtCoder/
    ├── AutoTester/
    │   ├── AutoTester.bat
    │   └── AutoTester.py
    └── ABC/
        ├── abc385/
        │   ├── a.py
        │   ├── b.py
        │   ├── c.py
        │   └── input.txt
        └── abc386/
            ├── a.py
            ├── b.py
            └── input.txt
```

下のpythonファイル(`AutoTester.py`)とバッチファイル(`AutoTester.bat`)を作成して、バッチファイルを実行すれば動作します。

```python:AutoTester.py
#親ディレクトリの親ディレクトリ内で最新の .py ファイルを見つけ、そのファイルを実行する。
import subprocess
import time
import os
import glob

def run_func_with_input():
    # Get the most recent .py file in the parent of the parent directory
    parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))    

    list_of_files = glob.glob(os.path.join(parent_dir, '**', '*.py'), recursive=True)

    # Remove the current file from the list
    list_of_files.remove(os.path.abspath(__file__))

    # Get the latest file
    latest_file = max(list_of_files, key=os.path.getmtime)
    input_data = ""

    input_file_path = os.path.join(os.path.dirname(latest_file), 'input.txt')
    if not os.path.exists(input_file_path):
        # Create input.txt with initial value "1 2 3"
        with open(input_file_path, 'w') as input_file:
            input_file.write("1 2 3")
        # Open the input.txt file in the default text editor
        os.startfile(input_file_path)

    with open(input_file_path, 'r') as input_file:
        # Check if input.txt exists in the latest file's directory
        input_data = input_file.read()

    # Check if the latest file was modified within the last 3 seconds
    if time.time() - os.path.getmtime(latest_file) > 3 \
        and time.time() - os.path.getmtime(input_file_path) > 3 : #ここデバッグ用
        print(".", end="")
        return

    print(f"Input:\n{input_data}")
    print(f"\n実行開始({"\\".join(latest_file.split(os.path.sep)[-2:])})")
    start_time = time.time()
    # Run the latest .py file
    result = subprocess.run(['python', latest_file], input=input_data, text=True)
    end_time = time.time()
    
    execution_time = end_time - start_time
    
    print(f"実行完了({execution_time * 1000:.2f}ms)")

if __name__ == "__main__":
    run_func_with_input()

```
```bat:AutoTester.bat
@echo off
set script_name=%~n0.py

:loop
python %script_name%
timeout /t 1 /nobreak >nul
goto loop
```
:::note info
「AutoTester.bat」は「自分と同じ名前の .py ファイル」を連続実行するプログラムなので、ファイル名を変更するときは両方とも拡張子より前が同じ名前になるようにしてください。
（例えば「AutoTester.py」を「hoge.py」に変える場合は、「AutoTester.bat」も「hoge.bat」に変更してください。）
:::

# コンテスト用コード生成セット

もう一つ、毎回コンテスト用のコードを生成するのダルいなと思っていたので、URLをコピペするだけでpyコードを自動で生成するやつも作りました。

例えば、
- `https://atcoder.jp/contests/abc379`
- `https://atcoder.jp/contests/abc379/tasks/abc379_e`
のような入力に対して、ABC/abc379/a.py ～ g.py を作成します。

ABCコンテストや、ARCコンテストはコンテスト形式ごとにまとめるようにしてあります。

さっきのディレクトリの直上に置いちゃってください。
```
.
└── AtCoder/
    ├── AtCoderコード生成.bat
    ├── AtCoderコード生成.py
    ├── AutoTester/
    │   ├── AutoTester.bat
    │   └── AutoTester.py
    └── ABC/
        ├── abc385/
        │   ├── a.py
        │   ├── b.py
        │   ├── c.py
        │   └── input.txt
        └── abc386/
            ├── a.py
            ├── b.py
            └── input.txt
```


```python:AtCoderコード生成.py
#AtCoderのURLを貼り付けることで、空の .py ファイルをA.py～G.pyまで作成する
import os

def create_files_from_url(url):
    # Create a directory based onthe contest name
    contest_name = url.split('/')[-1]
    print(contest_name)

    #個別の課題ページを入力した場合の処理
    contest_name = contest_name.split('_')[0]
    print(contest_name)

    typical_contests = ['abc', 'arc', 'agc']
    for typical_contest in typical_contests:
        if contest_name.startswith(typical_contest):
            contest_name = os.path.join(typical_contest.upper(), contest_name)
            break

    if not os.path.exists(contest_name):
        os.makedirs(contest_name)
    
    # Create empty files A.py to G.py
    for letter in 'ABCDEFG':
        file_path = os.path.join(contest_name, f'{letter}.py')
        if not os.path.exists(file_path):
            with open(file_path, 'w') as file:
                pass
            print(f'Created {file_path}')

# Example usage
url = input("Enter the AtCoder contest URL\n (ex.https://atcoder.jp/contests/abc777)\n: ")
create_files_from_url(url)


```
```bat:AtCoderコード生成.bat
@echo off
set script_name=%~n0.py

:loop
python %script_name%
goto loop
```

# いざ実行

:::note warn
ずっと表示し続けると見づらいですから、最新のpythonファイルの更新が３秒以前の場合は「.」を出力するようにしていますので、ピリオドしか出力されなくても怒らないでください。
:::

以下のコードを作って保存してみます。
```python:ABC\abc386\a.py
A = list(map(int, input().split())) 
print(sum(A))
```
すると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2280823/333bf2dd-cc5e-4c17-7efd-022fc3e4caf1.png)
無事に出力できました。

仕様として何回か（３回程度）連続して実行されるようになっています。

また、pythonファイルを作った場所にinput.txtがない場合は「1 2 3」と適当な内容を入れて作成し、テキストを開くようにしてあります。


# 蛇足
自分はコンテスト参加時、問題ページとVisualStudio、今回紹介したコードテスターとinput.txtのテキストエディタを開いています。みなさんはどうでしょうか。

あと「そもそもアレを使えばこの記事のコードいらないよ！」とかあったら教えてください。
競プロ仲間も募集中です。

では。