RustとCI/CDに入門する

これは[株式会社POL テックカレンダー 2021](https://qiita.com/advent-calendar/2021/pol) 16日目の記事です。前日の記事は[こちら](https://qiita.com/ryu19-1/items/7d81bec07595ba4c8c5f)です。

# はじめに

弊社での新サービスにRustを採用することが決まり、勉強のために[詳解Rustプログラミング](https://amzn.to/3q1Abl0)や[実践Rustプログラミング入門](https://amzn.to/33ynjvl)を読んでいます。初めての言語を学ぶ上でなにか形のあるものを作りたいと思い、[ここ](https://rust-cli.github.io/book/index.html)を参考にすごく簡単なCLIアプリを作りました。

ただ作るだけだと味気ないので、CI/CDの勉強とかっこよくREADMEにバッジをつけるためにコードカバレッジの設定をしました。ついでにパッケージを配布できるようにもしました。
この記事でRustaceanへの一歩を踏み出していただけると嬉しいです。

## この記事のゴール

この記事では、Rustを使ってすごく簡単なCLIアプリを作成し、GitHub Actionを使ってテストを自動化し、Codecovを使ってコードカバレッジを測定します。作成したアプリは[ここ](https://crates.io/crates/srgat)で配布されています。
作成したCLIアプリは[GitHub](https://github.com/w40141/srgat)にあります。

# CLIアプリ
この記事ではmacOSを使います。

## 環境構築
コマンドを順に実行することで環境構築ができるようになっています。

### Rust
Rustの導入にはrustupというコマンドラインツールを使用します。
公式では以下のコマンドを推奨していますが、私はHomeBrewで入れました。

```
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
```

```
brew install rustup
```

NeovimやVScodeの補完や型情報の表示のためにRust Language Server (rls)を導入することをおすすめします。

```
rustup component add rls rust-analysis rust-src
```

### Cargo
RustにはCargoというビルドシステム兼、パッケージマネージャがあります。この記事でもCargoを使ってプロジェクトを作ります。srgatは今回のCLIアプリの名前です。

```
cargo new srgat --bin
cd srgat
```

そのままのCargoでは他のパッケージ（Rustではクレートと呼びます。）を入れるために、プロジェクト内のCargo.tomlにクレートの情報を記述する必要があります。これでは面倒なので、便利にするためにcargo-editを導入します。

```
cargo install cargo-edit
```

cargo-editによりCargoのサブコマンドとして、add、rm, upgrade等が使えるようになります。
今回のプロジェクで使うクレートを以下のようにインストールします。

```
// CLIアプリを作るためのクレート (ver. 0.3.25)
cargo add structopt
// 正規表現のクレート (ver. 1.5.4)
cargo add regex
```

RustでCLIアプリを作る場合はClapというクレートが有名ですが、[これ](https://rust-cli.github.io/book/tutorial/cli-args.html)にならいstructoptを使います。
他にも[fncmd](https://github.com/yuhr/fncmd)や[argopt](https://crates.io/crates/argopt)という魅力的なクレートもあります。

## CLIアプリの作成
今回は作成するCLIアプリはソースコード内にあるTODO:、NOTE:などをタグ情報とし、それらタグ情報を画面上に出力するアプリです。
小さなアプリなので、ソースコードとテストコードはすべてsrc/main.rsに記述します。

```rust:src/main.rs
use regex::Regex;
use std::{fs::read_to_string, path::PathBuf};
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
#[structopt(name = "srgat", about = "tag search for source code")]
pub struct Cli {
    /// Pring tags in the files
    #[structopt(short, long, parse(from_os_str))]
    files: Option<Vec<PathBuf>>,
}

fn find_tags(num: usize, line: &str) {
    let tags = [
        "TODO: ",
        "INFO: ",
        "FIX: ",
        "WARNING: ",
        "NOTE: ",
        "HACK: ",
        "PERF: ",
    ];
    for tag in tags {
        if find_tag(line, tag) {
            print_line(num, line)
        }
    }
}

fn find_tag(line: &str, tag: &str) -> bool {
    let re = Regex::new(tag).unwrap();
    let contains_tag = re.find(line);
    match contains_tag {
        Some(_) => true,
        None => false,
    }
}

#[test]
fn test_find_tag() {
    let result = find_tag("TODO: This is a todo.", "TODO: ");
    assert_eq!(result, true);
    let result = find_tag("NOTE: This is a NOTE.", "NOTE: ");
    assert_eq!(result, true);
}

fn run_files(v: Vec<PathBuf>) {
    read_files(v)
}

fn read_files(vp: Vec<PathBuf>) {
    for v in vp {
        read_file(v);
        // NOTE: 改行
        println!("");
    }
}

fn read_file(p: PathBuf) {
    println!("{:?}", p);
    let content = read_to_string(&p).expect("could not read file");
    read_lines(content)
}

fn read_lines(content: String) {
    for (i, line) in content.lines().enumerate() {
        let num = i + 1;
        find_tags(num, line)
    }
}

fn print_line(num: usize, line: &str) {
    println!("{}: {}", num, line);
}

// TODO: showをerror表示から変える
fn run_show() {
    println!("error")
}

// TODO: すっきりさせる
fn run(cli: Cli) {
    match cli.files {
        Some(v) => run_files(v),
        None => run_show(),
    }
}

fn main() {
    let cli = Cli::from_args();
    run(cli)
}
```

structoptは以下のようにパラメータを簡単に追加できます。shortは-f、longは--filesを設定します。他にも色々な[設定の仕方](https://rust-cli.github.io/book/tutorial/cli-args.html)があります。

```rust
#[derive(StructOpt, Debug)]
#[structopt(name = "srgat", about = "tag search for source code")]
pub struct Cli {
    /// Pring tags in the files
    #[structopt(short, long, parse(from_os_str))]
    files: Option<Vec<PathBuf>>,
}
```

使い方は以下を想定しています。不完全なアプリですが、目的とするタグ情報は表示されました。

```
srgat -f src/main.rs
"src/main.rs"
15:         "TODO: ",
16:         "INFO: ",
17:         "FIX: ",
18:         "WARNING: ",
19:         "NOTE: ",
20:         "HACK: ",
21:         "PERF: ",
41:     let result = find_tag("TODO: This is a todo.", "TODO: ");
43:     let result = find_tag("NOTE: This is a NOTE.", "NOTE: ");
54:         // NOTE: 改行
76: // TODO: showをerror表示から変える
81: // TODO: すっきりさせる

```

# CI/CD入門
今回はCI/CDツールとしてGitHub Action、コードカバレッジの管理にCodecovを使います。

## GitHub Actionの設定
GitHub Actionの導入にはレポジトリのタブにあるActionタブを押すとレコメンドが表示されます。
Set up this workflowをクリックすると自動でworkflowが作成されます。GitHub Acitonは無料で使えますが、いろいろな制限があります。入門だけなら特に細かい設定はいらないと思いますが、この設定ではUbuntuでビルドとテストが走るので、注意する必要があるかもしれません。
![スクリーンショット 2021-12-15 22.11.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20556/c63de30f-31e2-2670-7c3a-4e2c6c8c551d.png)

## コードカバレッジ
Rustのコードカバレッジを測定するツールとして[cargo-kcov](https://github.com/kennytm/cargo-kcov)、[tarpaulin](https://github.com/xd009642/tarpaulin)、[grcov](https://github.com/mozilla/grcov)があります。cargo-kcovはメンテナンスがされていないようなので、今回はtarpaulinとおまけとしてgrcovを使います。

### Codecov
[Codecov](https://about.codecov.io/)を使ってGitHub Actionと連携するためにGitHubアカウトでログインし、Repository Upload Tokenを取得する必要があります。右にあるnot set yetボタンを押すとまだ連携していないレポジトリが一覧で出ます。連携が必要なレポジトリをクリックすると以下のようにtokenが発行されるので、コピーします。
![スクリーンショット 2021-12-15 22.35.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20556/935975eb-16b7-3878-0ef5-851d741fc1e9.png)

コピーしたtokenはCodecovと連携したレポジトリのSetting > Secrets > New repository > secretでペーストします。こうすることで、workflow中で環境変数としてtokenが使えます。

### tarpaulin
tarpaulinを使ってコードカバレッジを測定し、Codecovで管理するために、上で作ったworkflowにコードカバレッジを測定するコードを書きます。全体として以下のようになります。

```yml
name: Rust

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

env:
  CARGO_TERM_COLOR: always

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Build
      run: cargo build --verbose
    - name: Run tests
      run: cargo test --verbose
    - name: Push to codecov.io
      env:
        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
      run: |
        cargo install cargo-tarpaulin
        cargo tarpaulin --out Xml
        bash <(curl -s https://codecov.io/bash) -X gcov -t $CODECOV_TOKEN

```

注意する点として、tarpaulinはx86_64プロセッサのlinux環境でしか動作しないようです。今回はUbuntu上で走らせることになるので、大丈夫です。

### grcov
grcovを使ってコードカバレッジを測定するためには[こちら](https://blog.esplo.net/entry/2019/04/grcov-script/)で記述されているコードを実行します（grcovはGitHub Action上で未確認です）。macOS上で動作させるために、lcovとgrcovをインストールします。

```
brew install lcov
cargo add grcov
``` 

## クレートの配布
作成したアプリはcargoを使うことで、[crates.io](https://crates.io/)で簡単に配布できます。
https://crates.io/me でアカウントを作成し、表示されたAPI tokenをつけてプロジェク内で以下のコマンドを叩くとビルドとアップロードが始まります。

```
cargo login xxxxxxxxxxxxxxxxxxxxxx
```

### READMEのバッジ
最後にREADMEにかっこよくバッジをつけましょう。現状は以下のようにバッジを付けています。
![スクリーンショット 2021-12-15 23.09.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20556/e18575c9-2459-84ad-1a06-024d82313b3f.png)
GitHub Actionのものは右上の三点リーダーにCreate status badgeボタンがあるので、READMEにコピペします。Codecovも同様に連携したレポジトリのSetting > Badgeにあるコードをコピペします。
また、[shieds.io](https://shields.io/)でオリジナルのバッジを作ることもできます。


## おわりに
とても簡単ですがRustとCI/CDに入門できました（気がします）。
Rustはまだまだ発展が著しいので、上記の設定やコードで不都合が起きる場合がありますが、ドキュメントが整備されているので、1次情報に当たるといいと思います。

12/17の記事はまーさんです。よろしくお願いします。