【Rust】エラーハンドリングの基本

実行した時にエラーになりうる関数はResultを返す。
Ok(File), Err(Error)など

use std::fs::File;

fn main() {
    let result = File::open("file.txt");
    match result {
        Ok(file) => {
            println!("ファイルのオープンに成功しました!");
        }
        Err(error) => {
            println!("ファイルのオープンに失敗しました!");
        }
    }
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.70s
Running `target/debug/app`
ファイルのオープンに失敗しました!

関数化すると

use std::fs::File;

#[derive(Debug)]
struct MyError {
    message: String,
}

fn main() {
    let result = open_file();
    println!("{:?}", result);
}

fn open_file() -> Result<File, MyError> {
    let result = File::open("file.txt");
    match result {
        Ok(file) => {
            return Ok(file);
        }
        Err(error) => {
            let error = MyError{ message: "ファイルのオープンに失敗しました!".to_string()};
            return Err(error);
        }
    }
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/app`
Err(MyError { message: “ファイルのオープンに失敗しました!” })

### エラーが発生した時、発生したエラーをそのまま返す

fn main() {
    let result = open_file();
    println!("{:?}", result);
}

fn open_file() -> Result<File, std::io::Error> {
    let result = File::open("file.txt");
    match result {
        Ok(file) => {
            return Ok(file);
        }
        Err(error) => {
            return Err(error);
        }
    }
}

“?”とすれば、その時点でエラーを返す。下記の方が簡略して書ける。

fn main() {
    let result = open_file();
    println!("{:?}", result);
}

fn open_file() -> Result<(), std::io::Error> {
    let result = File::open("file.txt");
    let file = result?;
    Ok(())
}

File::open(SCHEDULE_FILE).unwrap()のunwrap()はエラーだたった時、エラー内容を表示してそのまま終了してしまう。
Result<(), std::io::Error> と書いた場合は、エラーだったときに、終了せずにエラーを返す。

【Rust】Unit Testの実用的な書き方

use std::{fs::File, io::{BufReader, BufWriter}};
use serde::{Serialize, Deserialize};
use clap::{Parser, Subcommand};
use chrono::NaiveDateTime;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Schedule {
    id: u64,
    subject: String,
    start: NaiveDateTime,
    end: NaiveDateTime,
}
impl Schedule {
    fn intersects(&self, other: &Schedule) -> bool {
        self.start < other.end && other.start < self.end
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Calendar {
    schedules: Vec<Schedule>,
}

const SCHEDULE_FILE : &str = "schedule.json";

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    List,
    Add {
        subject: String,
        start: NaiveDateTime,
        end: NaiveDateTime,
    }
}

fn main() {
    let options = Cli::parse();
    match options.command {
        Commands::List => show_list(),
        Commands::Add { subject, start, end }
            => add_schedule(subject, start, end),
    }
}

fn show_list() {
    let calendar : Calendar = {
        let file = File::open(SCHEDULE_FILE).unwrap();
        let reader = BufReader::new(file);
        serde_json::from_reader(reader).unwrap()
    };

    println!("ID\tSTART\tEND\tSUBJECT");
    for schedule in calendar.schedules {
        println!(
            "{}\t{}\t{}\t{}",
            schedule.id, schedule.start, schedule.end, schedule.subject
        );
    }
}

fn add_schedule(
    subject: String,
    start: NaiveDateTime,
    end: NaiveDateTime,
) {
    let mut calendar : Calendar = {
        let file = File::open(SCHEDULE_FILE).unwrap();
        let reader = BufReader::new(file);
        serde_json::from_reader(reader).unwrap()
    };

    let id = calendar.schedules.len() as u64;
    let new_schedule = Schedule {
        id,
        subject,
        start,
        end,
    };

    for schedule in &calendar.schedules {
        if schedule.intersects(&new_schedule) {
            println!("エラー: 予定が重複しています");
            return;
        }
    }
    calendar.schedules.push(new_schedule);

    {
        let file = File::create(SCHEDULE_FILE).unwrap();
        let writer = BufWriter::new(file);
        serde_json::to_writer(writer, &calendar).unwrap();
    }
    println!("予定を追加しました。");
}

#[cfg(test)]
mod tests {
    use super::*;

    fn naive_date_time(
        year: i32,
        month: u32,
        day: u32,
        hour: u32,
        minute: u32,
        second: u32,
    ) -> NaiveDateTime {
        chrono::NaiveDate::from_ymd_opt(year, month, day)
            .unwrap()
            .and_hms_opt(hour, minute, second)
            .unwrap()
    }

    fn test_schedule_intersects(
        h0: u32,
        m0: u32,
        h1: u32,
        m1: u32,
        should_intersects: bool
    ) {
        let schedule = Schedule {
            id: 0,
            subject: "既存予定1".to_string(),
            start: naive_date_time(2024, 1, 1, h0, m0, 0),
            end: naive_date_time(2024, 1, 1, h1, m1, 0),
        };
        let new_schedule = Schedule {
            id: 999,
            subject: "新規予定".to_string(),
            start: naive_date_time(2024, 1, 1, 19, 0, 0),
            end: naive_date_time(2024, 1, 1, 20, 0, 0),
        };
        assert_eq!(should_intersects, schedule.intersects(&new_schedule));
    }
    

    #[test]
    fn test_schedule_intersects_1(){
        test_schedule_intersects(18, 15, 19, 15, true)
    }

    #[test]
    fn test_schedule_intersects_2(){
        test_schedule_intersects(19, 45, 20, 45, true)
    }
}

$ cargo test
Compiling calendar v0.1.0 (/home/vagrant/dev/work/rusty/calendar)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.40s
Running unittests src/main.rs (target/debug/deps/calendar-867e0c10670d3913)

running 2 tests
test tests::test_schedule_intersects_2 … ok
test tests::test_schedule_intersects_1 … ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

なるほど、これはかなり参考になる。

【Rust】ユニットテスト

テストを実行するには、
$ cargo test
を実行する

fn main() {
    println!("This is test code");
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2+2, 4);
    }
}

Running unittests src/main.rs (target/debug/deps/sample-a78b7dbded83e75d)

running 1 test
test tests::it_works … ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

作成したfunctionのテストを行う

#[cfg(test)]
mod tests {
    use {
        super::*,
    };

    #[test]
    fn test_quick_sort() {
        let data = vec![6, 15, 4, 2, 8, 5, 11, 9, 7, 13];
        let expected = vec![2, 4, 5, 6, 7, 8, 9, 11, 13, 15];
        assert_eq!(expected, quick_sort(data));
    }
}

note: to see what the problems were, use the option `–future-incompat-report`, or run `cargo report future-incompatibilities –id 1`
Running unittests src/main.rs (target/debug/deps/sample-a78b7dbded83e75d)

running 1 test
test tests::test_quick_sort … ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

assert!, assert_eq!, assert_ne!がある。

うおおおおおおおおおお、これは超絶勉強になるな…

macでjmeterを使いたい

Test Plan -> Add -> Thread(Users) -> Thread Group

HTTP Request
HTTP RequestのGet, ip, path, paramなどを設定する

Thread Group
Thread Propertiesの Threads(リクエスト数), Ramp-up period(リクエストの作成期間), Loop Count(リクエスト量)を設定する

Listenerの追加: テスト結果を表示
Summary Reportを追加

なるほど、一通りの機能はわかったが、複数画面はどうやるんだろうか?

スタブを使ってテストをしたい

外部連携APIを開発する際に、環境がないといったケースに対応するためスタブってテストを行う。そのために簡単なスタブを構築します。

<?php
/* スタブAPIが受け付けるPOSTパラメータ:name */

header('Access-Controll-Allow-Origin: *');
// header('Access-Controll-Allow-Origin: http://192.168.56.10:8000/post.php');
header('Access-Controll-Allow-Credentials: false'); // Basic認証やCookieのやりとりをする場合に必要
header('Access-Controll-Allow-Headers: Content-Type');
header('Content-Type: application/json; charset=utf-8');

date_default_timezone_set('Asia/Tokyo');

if(isset($_POST['name']) === false || $_POST['name'] === ''){
	$_POST['name'] = 'TEST_API';
}

$postName = htmlspecialchars($_POST['name'], ENT_QUOTES);

$array = [
	'name' => $postName . '_RECIEVED',
	'date' => date("Y-m-d H:i:s"),
];

$json = json_encode($array);

// $file = new SqlFileObject('log.txt', 'a');
// $file->fwrite(
// 	"【→API】RequestParameter:" . $postName .  "'\n【←API】ReturnParameter :" . $json . "\n----------\n"
// );

echo $json;
exit;
$url = "http://192.168.56.10:8000/api.php";

// 設定するHTTPヘッダフィールド
$headerdata = array(
	'Content-Type: application/json',
	'X-HTTP-Method-Override: GET'
);

$param = array(
	"name" => "taro"
);

$postdata = json_encode($param);

$ch = curl_init($url);
$options = array(
	CURLOPT_POST => true,
	CURLOPT_RETURNTRANSFER => true,
	CURLOPT_HEADER => true,
	CURLOPT_HTTPHEADER => $headerdata,
	CURLOPT_POSTFIELDS => $postdata
);
curl_setopt_array($ch, $options);

$response = curl_exec($ch);
$response_info = curl_getinfo($ch);
$response_code = $response_info['http_code'];
$response_header_size = $response_info['header_size'];
curl_close($ch);

if($response_code == 200){
	print "[Result] success.\n";
} else {
	print "[Result] failed [$response_code]. \n";
}

$response_body = substr($response, $response_header_size);
print "[ResponseData]\n".trim($response_body);

なるほど、このような仕組みなのか…

Xdebugとは

XdebugはPHPのエクステンションでデバッグ機能を提供
– スタックの追跡
– var_dumpを整形
– コードのボトルネックを提供

なるほど、vscodeでのxdebugも概要はなんとなく理解した

[負荷試験] jmeterを使ってみる

JMeterはapacheが開発しているオープンソースの負荷検証ツール
java8以上が必要

### UbuntuにJMeterをインストール
$ sudo apt install jmeter
あれ、エラーになるな…

macに入れてみる

thread groupを作成し、その後、 HTTP requestを設定する

Number of Threads(users), Ramp-up period(seconds), Loop Countを設定する
Ramp-Up期間が何秒かけてスレッドを送信するか

なるほど、考え方は多少わかった。

Apache Benchを使ってみる

-nには、Totalで発行するリクエスト数を指定
-cには、同時接続数を指定

$ ab -n -c <同時接続数>
Complete requestsがリクエストに成功

$ ab -n 100 -c 100 -P id:password https://*.co.jp/ 

$ ab -n 100 -c 100 https://www.google.com/
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking www.google.com (be patient)…..done

Server Software: gws
Server Hostname: www.google.com
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-ECDSA-CHACHA20-POLY1305,256,256
Server Temp Key: X25519 253 bits
TLS Server Name: www.google.com

Document Path: /
Document Length: 15095 bytes

Concurrency Level: 100
Time taken for tests: 5.690 seconds
Complete requests: 100
Failed requests: 99
(Connect: 0, Receive: 0, Length: 99, Exceptions: 0)
Total transferred: 1611214 bytes
HTML transferred: 1505980 bytes
Requests per second: 17.58 [#/sec] (mean)
Time per request: 5689.514 [ms] (mean)
Time per request: 56.895 [ms] (mean, across all concurrent requests)
Transfer rate: 276.55 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 534 1480 684.7 1113 2959
Processing: 189 1196 500.7 1350 3571
Waiting: 120 720 301.6 563 1339
Total: 723 2677 799.6 2599 5033

Percentage of the requests served within a certain time (ms)
50% 2599
66% 2936
75% 2958
80% 2977
90% 3549
95% 5022
98% 5032
99% 5033
100% 5033 (longest request)

Requests per second:17.58 [#/sec] (mean) が1秒間に捌けるリクエスト
Time per request: が1秒間に捌けるリクエスト

なるほど、apache benchの概念を理解した

テストコードの方針を考える

### テストコードの目的
– テストなしにリリースするのが不安な時
– 保守性の向上
– 障害時に不具合をテストコードで再現する

### テストコードの対象
– セキュリティ上重要な箇所(権限管理、決済、メール)
– 重要度の高いユースケース
– 複雑なロジックやトリッキーなコード
– 止むを得ずパッチを当てたところ
– 自動化した方が早いところ(手作業でのアカウント作成などを省略する)
– 例外処理(意図的にバグを発生させる時)
※シンプルなロジックや重要度の低いコードのテストは優先度が下げるか省略も検討する

### テストコードの留意点
– 閾値の境界、条件分岐の網羅
– before afterを検証する
– テストコードはシンプルにする
– privateメソッドはテストしない

なるほど、枠組みは大体理解しました。