AtCoderのテストと提出を自動化する【Python × VSCode】

:::note info
同じ記事をZennでも公開しています！
:::

https://zenn.dev/0w0kaomoji/articles/cd50ada8c2d3c2


AtCoderのテストを自動化し、さらに自動で提出できるように環境を整えました。

:::note warn
生成AIはコードのコメント付けにのみ使用しています。
:::
## 1. 目的

- テストをするときに**いちいちコピペをするのが面倒**なので、テストを自動化したい。
- あわよくば、**提出も**コマンドラインで行いたい。
- できるだけ**シンプルに**（当社比）行いたい。

## 2. 課題

- 参考になりそうな記事はどれも**３年以上前**のもので、古い。
- 自分の環境に**ぴったり合う記事はない**ので試行錯誤する必要がある。
- **システムケースをダウンロード**したいが、参考になる記事が少ない。

## 3. 環境

- Windows 11
- Visual Studio Code 1.94.2
- Python 3.11.3 (.venv)

## 4. テストと提出を自動化する

### 1. 「AtCoder」フォルダを作る

- 解答コードは「abc001_a.py」のように「**コンテスト名_問題名.py**」としておきます。
    
    ```
    親フォルダ/
    ├── .venv/                    # 仮想環境（ある場合）
    └── AtCoder/
        ├── script/               # メインのプログラムファイルを格納
        └── src/
        │   └── abc001_a.py       # 問題の解答コード
        └── test/                 # コンテストのサンプルテストケースを格納
    ```
    

### 2. **online-judge-tools**をインストールする

- インストール後、AtCoderにログインします。
    
    ```powershell
    pip install online-judge-tools
    oj login -u {ユーザー名} -p {パスワード} "https://atcoder.jp/"
    oj login --check "https://atcoder.jp/"
    ```
    

### 3. **scriptフォルダの下**に「cptest.ps1」ファイルを作る

- フォルダ構造は以下のようになります。
    
    ```
    親フォルダ/
    ├── .venv/                    
    └── AtCoder/
        ├── script/
        │   └── cptest.ps1        
        └── src/
        │   └── abc001_a.py       
        └── test/                 
    ```
    
- ここで、**Dropboxのアクセストークン**を取得する必要があります。
    - 取得方法
        1. [Login - Dropbox](https://www.dropbox.com/developers/apps/create?_tk=pilot_lp&_ad=ctabtn1&_camp=create) で新しいアプリを作ります。（名前や設定はなんでもOK）
        2. アプリを作ったら、Permissionsタブへ行き、**files.metadata.read**と**sharing.read**を有効にします。
        3. Settingsタブへ戻って、OAuth 2 の欄の「Generate」ボタンを押します。
        :::note warn
        トークンは数時間で期限切れになるので、**再取得する**必要があります。
:::
        
- 「**cptest.ps1**」ファイルには以下のように書きます。
     <details><summary>コードと説明</summary>
                
    1. 仮想環境を有効化する
    2. 引数を取得する
    3. testフォルダ下に対象とするフォルダが存在しない場合、テストケースをダウンロードする。
        - import_test.pyは**次のステップ**で作成します。
    4. 時間制限を取得する。
    5. テストを実行する。
    6. 全てACとなると提出される。（確認メッセージが出る）
    7. 問題が解き終わると、テストケースを削除する。
        
        ```powershell
        # 仮想環境をアクティベート (存在する場合)
        . ".venv/Scripts/Activate.ps1"  # 仮想環境がある場合はアクティベート
        
        # 引数から問題名を取得
        $problem_name = $Args[0]        # スクリプト実行時に渡された問題名を取得
        
        # コンテストURLとテストケース格納ディレクトリを生成
        $base_url = ($problem_name.Replace("_", "-")).Substring(0, ($problem_name.Length) - 2)   # 問題名からベースURLを生成 (例: abc001_a -> abc001)
        $test_dir = "Atcoder/test/$problem_name"                                                 # テストケースを格納するディレクトリのパス
        
        # Dropboxアクセストークン (セキュリティ上、実際のトークンは伏字にしています)
        $dropbox_token = "XXXXX"  # (伏字)
        
        # 問題のURLを生成
        $url = "https://atcoder.jp/contests/$base_url/tasks/$problem_name"
        
        # テストケースのフォルダが存在しなければ作成し、テストケースをダウンロード
        if (! (Test-Path $test_dir)) {
          oj dl -d $test_dir $url  # "oj dl" コマンドでテストケースをダウンロード
          python Atcoder/script/import_test.py $problem_name $base_url $test_dir $dropbox_token  # import_test.py スクリプトで Dropbox からテストケースをダウンロード
        }
        
        # "oj-api" コマンドで問題の詳細を取得 (oj-apiコマンドは別途インストールが必要)
        $ojOutput = (oj-api get-problem $url)
        
        # 取得した出力を JSON オブジェクトに変換
        $jsonObject = ConvertFrom-Json $ojOutput
        
        # タイムリミットを取得 (ミリ秒単位なので秒に変換)
        $tle = $jsonObject.result.timeLimit / 1000
        
        # テストの実行　&&　提出　&&　テストデータのフォルダを削除
        oj t -c "python Atcoder/src/$problem_name.py" -d $test_dir/ --no-print-input -t $tle  && oj s $url Atcoder/src/$problem_name.py --language 5078  && Remove-Item -Recurse -Force $test_dir
        ```
</details>
    
### 4. **scriptフォルダの下**に「import_test.py」ファイルを作る

- フォルダ構造は以下のようになります。
    
    ```
    親フォルダ/
    ├── .venv/                    
    └── AtCoder/
        ├── script/
        │   ├── cptest.ps1 
        │   └── import_test.py      
        └── src/
        │   └── abc001_a.py       
        └── test/                 
    ```
    
- 「**import_test.py**」ファイルには以下のように書きます。
    <details><summary>コードと説明</summary>
    
    1. Power Shellから引数を受け取る。
    2. Dropboxからダウンロードするための設定をする。
    3. ファイル保存のための関数を定義する。
    4. サンプルテスト以外をダウンロードする。
        
        ```python
        import dropbox 
        import requests  
        import os  
        import sys
        import concurrent.futures
        
        # コマンドライン引数を受け取る
        problem_name = sys.argv[1]  # ABC001_A
        base_url = sys.argv[2]      # ABC001
        test_dir = sys.argv[3]      # Atcoder/test/ABC001_A
        dropbox_token = sys.argv[4]  # Dropboxのアクセストークン
        
        # Dropboxからダウンロードするための設定
        dbx = dropbox.Dropbox(dropbox_token, scope=["sharing.read", "files.metadata.read"])
        SHARED_URL = "https://www.dropbox.com/sh/arnpe0ef5wds8cv/AAAk_SECQ2Nc6SVGii3rHX6Fa?dl=0"  # テストケースの共有フォルダのURL
        
        def download_file(testcase_name, directory, case_type):
            """指定ファイルをダウンロードして保存する関数"""
            # DropboxからファイルをダウンロードするためのURLを取得
            data = dbx.sharing_get_shared_link_file(
                url=SHARED_URL,
                path=f"/{base_url}/{problem_name[-1]}/{case_type}/{testcase_name}"
            )
            download_url = data[0].url.replace("dl=0", "dl=1")
        
            # ダウンロードしたファイルを保存
            res = requests.get(download_url)
            with open(f"{directory}/{testcase_name}.{case_type}", 'wb') as f:
                f.write(res.content)
        
        # Dropboxからテストケースのリストを取得
        in_testcases = dbx.files_list_folder(
            path=f"/{base_url}/{problem_name[-1]}/in",
            shared_link=dropbox.files.SharedLink(url=SHARED_URL, password=None)
        )
        
        # 並列処理でファイルをダウンロード
        with concurrent.futures.ThreadPoolExecutor() as executor:
            futures = []
            for testcase in in_testcases.entries:
                # サンプルテスト以外をダウンロード
                if not testcase.name.startwith(00):
                    in_file_path = os.path.join(test_dir, f"{testcase.name}.in")
                    out_file_path = os.path.join(test_dir, f"{testcase.name}.out")
        
                    futures.append(executor.submit(download_file, testcase.name, test_dir, "in"))
                    futures.append(executor.submit(download_file, testcase.name, test_dir, "out"))
            # 全てのダウンロードタスクが完了するまで待つ
            concurrent.futures.wait(futures)
        ```
</details>        

### 5.  「tasks.json」ファイルを作って`Ctrl + Shift + B`**にテスト実行を割り当てる**

- **Terminal（ターミナル）→ Configure Tasks（タスクの構成）→ Other**とすると、「tasks.json」が「.vscode」フォルダ下に作成されます。（「.vscode」フォルダは自動的に作成されます。）
- その結果、フォルダ構造は以下のようになります。
    
    ```
    親フォルダ/
    ├── .venv/                    
    ├── .vscode/
    │   └── tasks.json            
    └── AtCoder/
        ├── script/
        │   ├── cptest.ps1        
        │   └── import_test.py   
        └── src/
        │   └── abc001_a.py      
        └── test/  
    ```
    
- 「**tasks.json**」ファイルには以下のように書きます。
    <details><summary>コードと説明</summary>
    1. シェルコマンドを実行する。
        - パスのスラッシュを機能させるために、スラッシュを二つ重ねる必要があります。
    2. コマンドにファイル名を引数として渡す。
        
    ```json
    {
      "version": "2.0.0",  // タスクファイルのバージョン
      "tasks": [
        {
          "label": "test_atcoder_sample",  // タスクの表示名
          "group": {
            "kind": "build",  // タスクの種類 (build:ビルド)
            "isDefault": true  // デフォルトで実行されるタスク
          },
          "type": "shell",  // シェルコマンドを実行する
          "command": "${workspaceFolder}\\Atcoder\\script\\cptest.ps1",  // 実行するコマンド
          "args": [
            "${fileBasenameNoExtension}"  // コマンドに渡す引数 (ファイル名)
          ]
        }
      ]
    }
    ```
</details>      

**お疲れ様です**！これで設定は終わりです。

## 5. 結果

- いよいよ**テストを実行**します。
- 先に設定した通り、テストをパスすると**自動で提出**されます。
- 解答ファイルを開いて`Ctrl + Shift + B` と入力します。
    <details><summary>スクリーンショット</summary> 
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3428921/6ca313a4-c969-7e80-fe93-a4ad4020cd75.png)
    このように「test」フォルダ下に新しくフォルダが作成され、テストケースが格納されます。
    ![image 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3428921/8eac1d6a-e6d1-f548-2582-0ce7cbf18418.png)
    ACとなると、ターミナルにはこのように出力されます。
    ![image 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3428921/509d96ad-7f24-59b1-5094-666f48e4d5e0.png)
    提出の前に、確認ダイアログが表示されるので、「abcc」と入力します。
    ![image 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3428921/b6308ba3-5468-5e55-fa03-62336db39bd6.png)
    勝手にコンテストの提出ページに飛ばされて、ACとなります。
</details>

    
- 無事に**テストと提出の自動化**に成功しました！
- 必要なパッケージは**1つ**、ファイルは**3つのみ**ということで、かなり**シンプル**な実装だといえると思います。

## コメント
テストめんどくさいなーという怠惰な理由ではじめましたが、思った以上に大変でした。一見すると万能なonline-judge-toolsでも、これを使えばすべて解決するというわけではありませんでした。oj dlコマンドのsystemというオプションでテストケースをすべてダウンロードできるはずだったのにうまくいかず、結局、ゼロからではないものの自分でスクリプトを書くはめになりました。とはいえ、これでしばらくは快適なAtCoder生活がおくれそうなので満足です。 October 25, 2024

## 参考
- Chokoryu. 2021. **“AtCoderで自動サンプルテストケース&手入力値テスト実行 With VS Code.”** Qiita. January 13, 2021. https://qiita.com/chokoryu/items/4b31ffb89dbc8cb86971.
    - 本記事に近い内容がわかりやすく説明されています。
- **“Login - Dropbox.”** n.d. Dropbox. https://www.dropbox.com/developers/apps/create?_tk=pilot_lp&_ad=ctabtn1&_camp=create.
    - アプリを作成するリンク。
- Online-Judge-Tools. 2024. **“GitHub - Online-judge-tools/Oj: Tools for Various Online Judges. Downloading Sample Cases, Generating Additional Test Cases, Testing Your Code, and Submitting It.”** GitHub. March 3, 2024. https://github.com/online-judge-tools/oj.
    - サンプルケースの取得、追加のテストケースの生成、テストの実行、コードの提出などを自動化するツール。
- Sugimoto Kaito. 2021. **“Dropbox API を駆使して AtCoder のテストケースを自動でダウンロードする.”** Zenn. October 14, 2021. https://zenn.dev/hellorusk/articles/copy-from-blog-20210211.
    - テストケースのダウンロードのために参考にしました。