Open5

Rust の tests ディレクトリのいろいろ

nukopynukopy

Rust の tests ディレクトリに tests/utils みたいに test utils 用のモジュールを作ると、tests/*.rs それぞれで utils の要素を全て import しないと unused 扱いになる。この例を示す。

ディレクトリ構造は以下:

./tests
├── bad_file.rs
├── core.rs
├── expected
│    ...
│   └── ...
├── general_flags.rs
├── inputs
│    ...
│   └── ...
└── utils
    ├── constants.rs
    ├── file.rs
    ├── mod.rs
    └── run.rs

utils::run モジュールの中身は以下:

use anyhow::Result;
use assert_cmd::Command;
use pretty_assertions::assert_eq;
use std::fs;

pub fn run(args: &[&str], expected_file: &str) -> Result<()> {
    let expected = fs::read_to_string(expected_file)?;
    let output = Command::cargo_bin(crate::BINARY_NAME)?
        .args(args)
        .output()
        .unwrap();
    assert!(output.status.success());

    let stdout = String::from_utf8(output.stdout).expect("invalid UTF-8");
    assert_eq!(stdout, expected);

    Ok(())
}

pub fn run_stdin(input_file: &str, args: &[&str], expected_file: &str) -> Result<()> {
    let input = fs::read_to_string(input_file)?;
    let expected = fs::read_to_string(expected_file)?;
    let output = Command::cargo_bin(crate::BINARY_NAME)?
        .write_stdin(input)
        .args(args)
        .output()
        .unwrap();
    assert!(output.status.success());

    let stdout = String::from_utf8(output.stdout).expect("invalid UTF-8");
    assert_eq!(stdout, expected);
    Ok(())
}

utils::run::runutils::run::run_stdintests/core.rs というインテグレーションテストでしか import されておらずそれ以外のテストでは import されていない。そのため、下記画像のように linter によって #[warn(dead_code)] という warning が表示されてしまう。

これは Rust のインテグレーションテストの仕様で、tests/*.rs がそれぞれ独立したクレートとして扱われることによる挙動である。

例えば、tests/core.rs クレート(あえて「クレート」と読んでいる)から見たら utils::run サブモジュールは使用されているが、(utils::run を import していない)bad_file.rs というクレートから見たら utils モジュールのサブモジュールである utils::run は未使用のモジュールということになり、#[warn(dead_code)] という warning が発生してしまう。

nukopynukopy

各 OSS リポジトリのテストディレクトリを見る

Awesome Rust で探す

https://github.com/rust-unofficial/awesome-rust?tab=readme-ov-file#observability

openobserve

https://github.com/openobserve/openobserve/tree/main/tests

test utils を #[cfg(test)] にぶち込んでる。でもこれは結局共通の test utils を定義できるわけではないからこれではないなぁ。

https://github.com/openobserve/openobserve/blob/main/tests/integration_test.rs

nukopynukopy

Deno

tests ディレクトリを workspace の member にして、1 つの独立したクレートとして扱っているパターン。

https://github.com/denoland/deno/blob/bb26fef4946e16a9b9916f61786416523d173e8a/tests/Cargo.toml

テストターゲットの tests/integration/mod.rs

#[path = "..."] 属性始めてみたけど、モジュール名を自由につけることができるようになる属性らしい。例えば、cache_tests.rs は何も属性を付けなければ mod cache_tests; と書く必要があるけど、これを mod cache; だけで良くなるようにできる。

#[path = "cache_tests.rs"]
mod cache;

https://github.com/denoland/deno/blob/bb26fef4946e16a9b9916f61786416523d173e8a/tests/integration/mod.rs

nukopynukopy

zulip に書いたやつを転載


Rust の integration test を書くときのディレクトリ構成について、調べてもわからなかった問題があったので質問させていただきます。

開発環境

$ rustc --version
rustc 1.80.1 (3f5fd8dd4 2024-08-06)

実現したいこと

  • Rust の integration test でテストユーティリティ用のモジュールを定義したい
  • ディレクトリ構造は cargo workspace を使わず、プロジェクトルートに srctests が置いてある基本的な構成にしたい
  • tests/utils にテストユーティリティ用のモジュールを作成したい。ただし、全ての tests/*.rs で import しなくても #[warn(dead_code)] を出ないようにしたい。

やったこと

背景

Rust で integration test を書く時、プロジェクトルートに tests ディレクトリを作成し、tests/*.rs を作成します。

このとき、テストユーティリティ用の tests/utils というモジュールを作成します。

例えば、以下のようなディレクトリ構成になります。

my-project
├── Cargo.toml
├── src
│   └── main.rs
└── tests
    ├── core.rs  # テスト
    ├── error.rs # テスト
    └── utils # テストユーティリティ用のモジュール
        ├── constants.rs
        ├── file.rs
        ├── mod.rs
        ├── random.rs
        └── run.rs

このとき、Rust の integration test の仕様上、tests/*.rs はそれぞれ独立したクレートとして扱われるため、utils の要素をそれぞれの tests/*.rs で全て import しないと tests/utils のモジュールの要素(定数や関数など)は unused 扱いとなり、コンパイル時に #[warn(dead_code)] が出てしまいます。

上記のディレクトリ構成の例でいうと、tests/core.rstests/error.rs の両方で import されている utils モジュールの要素は unused 扱いになりませんが、tests/core.rs だけで使われている utils モジュールの要素は tests/error.rs で使われていないとコンパイラに判断され、unused 扱いとなり、#[warn(dead_code)] の warning が出てしまいます。

回避策

これを回避する方法として、試したのは以下の 3 つです。しかし、どれも納得の行く解決方法ではありませんでした。

  • tests/utils/mod.rs#![allow(dead_code)] を記述する
    • → 本質的な解決方法でないため、できればやりたくない
  • src/... をライブラリクレートにし、テストユーティリティ用のモジュールを定義し、tests/*.rs で import する
    • → テストユーティリティなので src 配下に置きたくない
  • cargo workspace を使用しテストユーティリティ用の member を作成した
    • src ディレクトリをなくし、メインロジック用の member を作成。
    • teststests/utils を member とし、tests/*.rstests/utils を import した
    • (こちらは Deno のリポジトリの Cargo.toml を参考にしました)
    • → こちらは苦肉の策でした。単純なプロジェクトなので、わざわざ cargo workspace を使いたくなかった。プロジェクトルートに srctests という構造を保ちながら、tests/utils にテストユーティリティ用のモジュールを作りたかった。

質問

まとめると、私が実現したいことは以下になります。

  • Rust の integration test でテストユーティリティ用のモジュールを定義したい
  • ディレクトリ構造は cargo workspace を使わず、プロジェクトルートに srctests が置いてある基本的な構成にしたい
  • tests/utils にテストユーティリティ用のモジュールを作成したい。ただし、全ての tests/*.rs で import しなくても #[warn(dead_code)] を出ないようにしたい。

実現する方法をもし知っている方がいらっしゃったらご回答いただければ幸いです!私が仕様を理解していない可能性もあるため、方向性が間違っているとか勘違いしてることがあればご指摘いただければありがたいです。