0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-10-26

同じ記事をZennでも公開しています!

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

生成AIはコードのコメント付けにのみ使用しています。

1. 目的

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

2. 課題

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

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にログインします。

    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 で新しいアプリを作ります。(名前や設定はなんでもOK)
      2. アプリを作ったら、Permissionsタブへ行き、files.metadata.readsharing.readを有効にします。
      3. Settingsタブへ戻って、OAuth 2 の欄の「Generate」ボタンを押します。

      トークンは数時間で期限切れになるので、再取得する必要があります。

  • cptest.ps1」ファイルには以下のように書きます。

    コードと説明
    1. 仮想環境を有効化する

    2. 引数を取得する

    3. testフォルダ下に対象とするフォルダが存在しない場合、テストケースをダウンロードする。

      • import_test.pyは次のステップで作成します。
    4. 時間制限を取得する。

    5. テストを実行する。

    6. 全てACとなると提出される。(確認メッセージが出る)

    7. 問題が解き終わると、テストケースを削除する。

      # 仮想環境をアクティベート (存在する場合)
      . ".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
      

4. scriptフォルダの下に「import_test.py」ファイルを作る

  • フォルダ構造は以下のようになります。

    親フォルダ/
    ├── .venv/                    
    └── AtCoder/
        ├── script/
        │   ├── cptest.ps1 
        │   └── import_test.py      
        └── src/
        │   └── abc001_a.py       
        └── test/                 
    
  • import_test.py」ファイルには以下のように書きます。

    コードと説明
    1. Power Shellから引数を受け取る。

    2. Dropboxからダウンロードするための設定をする。

    3. ファイル保存のための関数を定義する。

    4. サンプルテスト以外をダウンロードする。

      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)
      

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」ファイルには以下のように書きます。

    コードと説明 1. シェルコマンドを実行する。 - パスのスラッシュを機能させるために、スラッシュを二つ重ねる必要があります。 2. コマンドにファイル名を引数として渡す。
    {
      "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}"  // コマンドに渡す引数 (ファイル名)
          ]
        }
      ]
    }
    

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

5. 結果

  • いよいよテストを実行します。

  • 先に設定した通り、テストをパスすると自動で提出されます。

  • 解答ファイルを開いてCtrl + Shift + B と入力します。

    スクリーンショット

    image.png
    このように「test」フォルダ下に新しくフォルダが作成され、テストケースが格納されます。
    image 1.png
    ACとなると、ターミナルにはこのように出力されます。
    image 2.png
    提出の前に、確認ダイアログが表示されるので、「abcc」と入力します。
    image 3.png
    勝手にコンテストの提出ページに飛ばされて、ACとなります。

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

コメント

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

参考

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?