読者です 読者をやめる 読者になる 読者になる

0x90

一回休み

JSのtestを実行するCargoの拡張機能を書いてみる

rust

連続投稿。RustのパッケージマネージャであるCargoのsubcommandを作るお話です。

Cargoとは

言わずと知れたRustのパッケージマネージャ兼make兼…のすごいやつです。このcargoですが、自分で勝手にsubcommandを追加できます。

Cargoは知らないサブコマンドが叩かれた場合、

$ cargo hoge

cargo-hogeという実行可能ファイルを~/.cargo/bin/から*1探そうとします。 ということで、ここにシェルスクリプトでもPythonでもバイナリでもなんでもおいてやればsubcommandが追加できます。

なお、本拡張機能を作るに当たり、小規模な拡張機能である

gitlab.com

を大いに参考にさせていただきました。ありがとございます。

なにを作るか

目的物の話が「続きを読む」のあとにあるというまさかまさかの構成ですが、今回作ってみたsubcommandは、RustのTestをnodeを使って実行するものです。

いままでずっとブログに書きためてきたように、いまRustからEmscriptenを呼ぶライブラリを作っているわけですが、そこで困るのはtestです。実はRustには素晴らしいテストフレームワーク(?)がくっついており、tests以下のディレクトリもしくは#[test]ディレクティブを持った関数を追加することで、かんたんにコードのテストができます。

qiita.com

とかが詳しいです。

なんですが、targetをasmjs-unknown-emscriptenにしてしまうと、できたJSファイルを実行可能ファイルとしてcargoは走らせようとしてしまいます。JSはShebangとかないので、困っちゃうわけです。

更に、JSとの連携テストもしたいので、予めいくらかのJS関数等をプレロードしておきたいと思いました。ということでこれを何とかするsubcommandを書いていきます。

できたもの

完成物はこちら。

github.com

紹介記事はQなんとかに書くとして、いくらか技術的なポイントを纏めておこうと思います。

Cargo.tomlの読み込み

今回、設定はCargo.tomlファイルに書くことにしました。

まず、Cargo.tomlの性質として、ユーザー定義の設定は、package.metadata以下でないといけないというのがあります。

[package.metadata.testjs]
node = "nodejs"
target = "asmjs-unknown-emscripten"
prelude = "tests/test.js"

ということで、こんな感じのセクションを追加することを想定するわけなのですが、問題はどうやってパースするかです。

tomlをパースするcrateは https://github.com/alexcrichton/toml-rs こんなのがあります。実はこのtoml-rs, serde_deriveとcustom deriveを組み合わせることで、

alexcrichton.com

niftyなコードを書くことができます。ただ、これをやろうとすると使いもしない構造体を2つも(Package & Metadata)定義する必要があり、鬱陶しいです。そこで今回はマクロを使って手でパースしました。

macro_rules! load_config {
    ($root:expr, $config:expr, $name:ident) => {
        if let Some(tmp) = $root.get(stringify!($name)) {
            $config.$name = tmp.as_str().expect(&format!("Invalid config for {}", stringify!($name))).to_owned();
        }
    }
}

macro_rules! load_config_option {
    ($root:expr, $config:expr, $name:ident) => {
        if let Some(tmp) = $root.get(stringify!($name)) {
            $config.$name = Some(tmp.as_str().expect(&format!("Invalid config for {}", stringify!($name))).to_owned());
        }
    }
}

fn load_config(path: &str) -> Config {
    let mut toml_file = File::open(path).expect(&format!("Unable to open {}", path));
    let mut t = String::new();

    toml_file.read_to_string(&mut t).expect(&format!("Unable to read {}", path));

    let toml_config = t.parse::<Value>().expect(&format!("Unable to parse {}", path));
    let mut config = Config { node: NODE.to_owned(), target: TARGET.to_owned(), prelude: None };

    if let Some(package) = toml_config.get("package") {
        if let Some(metadata) = package.get("metadata") {
            if let Some(testjs) = metadata.get("testjs") {
                load_config!(testjs, config, node);
                load_config!(testjs, config, target);
                load_config_option!(testjs, config, prelude);
            }
        }
    }

    config
}

const TARGET: &'static str = "asmjs-unknown-emscripten";
const NODE: &'static str = "node";
#[derive(Default, Debug)]
struct Config {
    node: String,
    target: String,
    prelude: Option<String>,

ちなみに、macro中では$nameはidentにしないとエラーです。Rustのマクロはきちっとしていてやはり光が強いです。*2

外部コマンドの実行と出力の取得

これは簡単で、std::process::Commandを使うだけでした。stdout, stderrをそのままつなぎたければ、

    Command::new("cargo")
            .args(&cargo_args)
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .spawn().expect("Cannot execute cargo")
            .wait().expect(&format!("Command cargo {} did not finish properly", cargo_args.join(" ")));

みたいな感じにします。

cargo test --no-runでできたファイルの在処

実は、決まっていやがりません。

github.com

仕方がないので、正規表現でそれっぽいファイル名を引っこ抜いてきて、更新時間が一番新しいものを使います。。

Cargo.tomlファイルに書くこと、及びcrate.ioへの登録

Rustのパッケージをcrates.ioに登録する | κeenのHappy Hacκing Blogを全面的に参考にさせていただきました…。ありがとうございます。

なお、他に何も書かなくとも、cargo installすると~/.cargo/bin(か、それに対応するどこか)へcargoは突っ込んでくれます。ありがとう…!

テストができるようになったのでライブラリ本体も捗りそうです。バグがあったらプルリクくださいね。。

*1:ちょっとこのディレクトリパスがどう決まっているかはよくわかってないw

*2:とはいえ使い過ぎはアレですが