AtCoderの問題で自作ライブラリを自動テストする方法

## はじめに



みなさん、競技プログラミングは楽しんでますでしょうか。

この記事では、**AtCoder の[テストケース](https://atcoder.jp/posts/20)を Dropbox から自動的にダウンロードし、競技プログラミング用のライブラリを検証するための GitHub Actions のワークフロー**を構築する手順を紹介します。

### 取り扱うこと
- AtCoder の問題で verify を実行する方法

### 取り扱わないこと
- online-judge-tools を使ったライブラリの実装
- Aizu Online Judge や Yosupo Judge の問題で自動テストを実行する方法








## 1. Dropbox アカウントとアプリの作成

まず手順に従って Dropbox アカウントを作成します。

https://www.dropbox.com/register

その後、Dropbox Developers の `Create App` からアプリを作成します。

https://www.dropbox.com/developers/apps

![graph7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/c51ce95a-1dc8-d200-9dfc-dd053c16d4dd.png)

![graph8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/82fbac8e-7f69-a8ba-f8e5-424401aca7e7.png)


















## 2. Dropbox API を使用した OAuth2 認証フロー

### App key と App secret の取得

アプリを作成したらまずは `Settings` タブから `App key` と `App secret` を保管します。
この値は他者に知られないように保管してください。

![graph10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/aa77396c-e04c-7bc0-1cc8-eb4b2e67c697.png)


### スコープの変更
次に `Permissions` タブからスコープを変更します。
次の項目にチェックを入れ、変更を保存します。

![graph9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/bfec4126-8010-98e7-4ddb-15d9b317d7b5.png)

### OAuth2 認証フロー

そしてアクセスコードを取得するために次のリンクにアクセスしてください。`$APP_KEY` は先ほど保管した `App Key` に置き換えてください。認証を許可して次の画面に表示されるアクセスコードを保管します。

https://www.dropbox.com/oauth2/authorize?client_id=$APP_KEY&response_type=code&token_access_type=offline


![graph11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/a1f1d476-7904-069b-bd47-48b2164317fe.png)


![graph12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/1db8cf6c-4b86-5488-471a-e38be50178b0.png)








## 3. リフレッシュトークンの取得と使用

### リフレッシュトークンの取得
リフレッシュトークンを取得するために、今まで保管した

- `$APP_KEY` (`App key`)
- `$APP_SECRET` (`App secret`)
- `$ACCESS_CODE` (アクセスコード)

を使用して次の `curl` コマンドを実行します。

```bash
$ curl https://api.dropbox.com/oauth2/token \
    -d code=$ACCESS_CODE \
    -d grant_type=authorization_code \
    -u $APP_KEY:$APP_SECRET
```

```json
{
    "access_token": ...,
    "token_type": "bearer",
    "expires_in": 14400,
    "refresh_token": ...,
    "scope": "account_info.read files.metadata.read sharing.read",
    "uid": ...,
    "account_id": ...
}
```

これでリフレッシュトークンが取得できます。
このリフレッシュトークンの役割は、アクセストークンの有効期限が切れた後に、新しいアクセストークンを取得するために使われるものです。**Dropbox API のアクセストークンの有効期限は14400秒と短いため、継続的に API を使うためにはリフレッシュトークンを経由する必要があります。**

### リフレッシュトークンの使用

次のコマンドでリフレッシュトークンを使用してアクセストークンを取得することができます。適宜 `$REFRESH_TOKEN` を先ほど取得したリフレッシュトークンに置き換えてください。
```bash
$ curl https://api.dropbox.com/oauth2/token \
    -d grant_type=refresh_token \
    -d refresh_token=$REFRESH_TOKEN \
    -u $APP_KEY:$APP_SECRET
```

```json
{
    "access_token": ...,
    "token_type": "bearer",
    "expires_in": 14400
}
```

この処理をこれから `GitHub Actions` のワークフローに組み込んでいきます。











## 4. GitHub Actions ワークフローの作成

### GitHub Secrets の設定

まずは、GitHub の Secrets に先ほど取得した Dropbox のトークンを登録します。

GitHub のリポジトリ設定ページに移動し、

![graph1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/e5210646-9f6e-3072-bbd1-c7898df32e41.png)

左メニューから「Secrets and variables」の「Actions」を選択します。

![graph2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/bee3c978-ad50-7876-d616-8623e9a030c0.png)

「New repository secret」ボタンをクリックし、

![graph3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/bd31cf1d-5e69-1314-e430-3f29562d2ec1.png)

先ほど取得した Dropbox の `App key` 、`App secret`、リフレッシュトークンを登録します（名前は `DROPBOX_APP_KEY` など)

![graph4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/f12d4f82-0c7e-1fb5-7cb9-5718ab5e3a04.png)

次のように登録されていれば設定完了です。

![graph5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/4c5ce6a1-5db3-e224-dd36-b9789ad44c69.png)

### ワークフロー設定ファイルの設定

次にワークフローの設定ファイルを編集します。

```diff_yaml:.github/workflows/verify.yml
name: verify

on: push

jobs:
  verify:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1

    - name: Set up Python
      uses: actions/setup-python@v1

    - name: Install dependencies
      run: pip3 install -U online-judge-verify-helper

+   - name: Refresh Dropbox Token
+     run: |
+      response=$(curl -X POST https://api.dropbox.com/oauth2/token \
+         -d grant_type=refresh_token \
+         -d refresh_token=${{ secrets.DROPBOX_REFRESH_TOKEN }} \
+         -u "${{ secrets.DROPBOX_APP_KEY }}:${{ secrets.DROPBOX_APP_SECRET }}" \
+         -H "Content-Type: application/x-www-form-urlencoded")
+       ACCESS_TOKEN=$(echo $response | jq -r '.access_token')
+       echo "::add-mask::$ACCESS_TOKEN"
+       echo "DROPBOX_TOKEN=$ACCESS_TOKEN" >> $GITHUB_ENV

    - name: Run tests
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        YUKICODER_TOKEN: ${{ secrets.YUKICODER_TOKEN }}
+       DROPBOX_TOKEN: ${{ env.DROPBOX_TOKEN }}
        GH_PAT: ${{ secrets.GH_PAT }}
      run: oj-verify all

```

先ほど行ったアクセストークンを取得する操作を追加して、GitHub の環境変数にアクセストークンを追加しています。

各操作の説明をします。

```yaml:.github/workflows/verify.yml
      run: |
        response=$(curl -X POST https://api.dropbox.com/oauth2/token \
          -d grant_type=refresh_token \
          -d refresh_token=${{ secrets.DROPBOX_REFRESH_TOKEN }} \
          -u "${{ secrets.DROPBOX_APP_KEY }}:${{ secrets.DROPBOX_APP_SECRET }}" \
          -H "Content-Type: application/x-www-form-urlencoded")
        ACCESS_TOKEN=$(echo $response | jq -r '.access_token')
```

`curl`コマンドでアクセストークンを含んだ json 形式のデータを取得、それに対して`jq`コマンドでアクセストークンだけ抜き出し、`$ACCESS_TOKEN` という変数に代入します。

```yaml:.github/workflows/verify.yml
        echo "::add-mask::$ACCESS_TOKEN"
```

ここで `$ACCESS_TOKEN` に `::add-mask::` をすることにより、この文字列がログに流れることを防ぎます。

```yaml:.github/workflows/verify.yml
        echo "DROPBOX_TOKEN=$ACCESS_TOKEN" >> $GITHUB_ENV
```

`$GITHUB_ENV` に `$ACCESS_TOKEN` を追加します。これでこの後のステップで `env.DROPBOX_TOKEN` を使うことが可能になります。















## 5. 実際に試してみる

ここまで作ったワークフローを実際に使って自動テストを試してみます。
今回はトポロジカルソートのライブラリを検証するため、ABC223のD問題を扱います。

https://atcoder.jp/contests/abc223/tasks/abc223_d

まずはライブラリです。

```C++:graph/topological_sort.hpp
#pragma once

#include <vector>
#include <queue>
#include "graph_template.hpp"

/**
 * @brief Topological Sort (トポロジカルソート)
 */
template<class T> std::vector<int> topological_sort(Graph<T> &graph) {
    int n = (int)(graph.size());
    std::vector<int> res, indeg(n, 0);

    for (int i = 0; i < n; i++) {
        for (auto c: graph[i]) {
            indeg[c]++;
        }
    }

    std::priority_queue<int, std::vector<int>, std::greater<int>> que;
    for (int i = 0; i < n; i++) {
        if (indeg[i] == 0) que.push(i);
    }

    while (!que.empty()) {
        auto v = que.top(); que.pop();
        res.push_back(v);
        for (auto c: graph[v]) {
            indeg[c]--;
            assert(indeg[c] >= 0);
            if (indeg[c] == 0) que.push(c);
        }
    }

    return res;
}
```

次にテスト用コードです。一行目に `#define PROBLEM "https://atcoder.jp/contests/abc223/tasks/abc223_d"` と記述し、テストに使う問題を明示しておきます。

```C++:verify/graph/topological_sort_1.test.cpp
#define PROBLEM "https://atcoder.jp/contests/abc223/tasks/abc223_d"
#include <bits/stdc++.h>
#include "graph/graph_template.hpp"
#include "graph/topological_sort.hpp"
using namespace std;
typedef long long int ll;
#define rep(i, N) for(int i = 0; i < (int)N; i++)

int main() {
    ll N, M; cin >> N >> M;
    Graph<ll> graph(N);
    rep (i, M) {
        ll a, b; cin >> a >> b; a--; b--;
        graph[a].push_back(b);
    }

    auto V = topological_sort(graph);
    if ((int)(V.size()) != N) {
        cout << -1 << endl;
        return 0;
    }
    rep (i, N) {
        if (i == 0) cout << V[i] + 1;
        else cout << ' ' << V[i] + 1;
    }
    cout << endl;
    return 0;
}
```

このコードをリモートブランチにプッシュして、

![graph6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3705454/d1088a7b-54e3-8b55-07cf-b273d1395f3a.png)

問題なくテストできました！アクセストークンも `--dropbox-token ***` となっており、伏せられていることがわかります。



## おわりに

この記事では、Dropbox API と GitHub Actions を活用し、AtCoder のテストケースを自動的にダウンロードして競技プログラミング用ライブラリをテストするワークフローの構築手順を紹介しました。これを導入することで、より幅広い問題でテストをすることができ、競技プログラミングのライブラリ整備がよりスムーズに進むようになると思います。

私自身もまだ始めたばかりですが、この自動化の一歩を踏み出すことで、より快適な自作ライブラリライフを一緒に送りましょう！

## 参考にさせていただいた記事

https://github.com/ruthen71/rcpl

https://zenn.dev/yakumo/articles/75d3df651d0609

https://qiita.com/KakurenboUni/items/b47d9e6e3582e2149d63