Open95

読者コミュニティ|runn クックブック

norionorio

ブック購入させていただきました。

テストの書き方について質問がございます。
非同期にDBレコードの値が変わるAPIのテストで使ってみたいと思いインストールしました。

下記のようなステップのテストを記述しています。

・API呼び出し
・DB参照

現状ですとAPI呼び出し後、DB参照がすぐに行われ、ステータスが変わる前の値を検証してしまいテストが通りません。

DB参照前に数秒Sleepさせることは可能なのでしょうか?

Ken’ichiro OyamaKen’ichiro Oyama

こんにちは。購入ありがとうございます。

DB参照前に数秒Sleepさせること

おそらく実施したいことは「ステータスが変わることを検証したい」だと思われます。

2つ紹介します。

例として「APIコール後に users テーブルの status カラムの値が 0 から別の値( 1 とか 2 など)になること」を確認するケースを考えます。

1. 単純にsleepさせる

Execランナーをつかって sleep コマンドを挟みます。

steps:
  - 
    req:
      /path/to/endpoint:
        post:
          # (snip)
  - 
    exec:
      command: sleep 5
  -
    db:
      query: SELECT status FROM users;
    test: current.rows[0].status != 0

2. ステータスが変化するまで確認する

loop: セクションを使用してリトライをします。

steps:
  - 
    req:
      /path/to/endpoint:
        post:
          # (snip)
  -
    db:
      query: SELECT status FROM users;
    loop: 
      interval: 5sec # 失敗したら5秒待つ
      count: 3 # 3回リトライする
      until: current.rows[0].status != 0

リトライについては「ステップのリトライを設定する 」で紹介していますのでご覧ください。

norionorio

ご返信ありがとうございます。
execランナーがまさにやりたいことでした。

ご回答ありがとうございます。

calloc134calloc134

初めまして。かろっくと申します。
昨日からrunnを使わせていただいております。このrunnの使い方について、いくつか質問失礼します。
runn run dummy.ymlを実行した際に、一つのシナリオごとにテストの成功/失敗を表示していますが、ステップごとの成功の可否を表示することはできますでしょうか?
また、ステップが一つ失敗したとき、以降のテストが実行されませんが、以降のテストが実行されるようなオプション等は存在しますか?
よろしくお願いいたします。

Ken’ichiro OyamaKen’ichiro Oyama

質問ありがとうございます!

一つのシナリオごとにテストの成功/失敗を表示していますが、ステップごとの成功の可否を表示することはできますでしょうか?

v0.64.1時点ではありません。シナリオの成功=シナリオ内の全ステップの成功(スキップ除く)なのでステップの成功失敗の表示はありません。

ステップが一つ失敗したとき、以降のテストが実行されませんが、以降のテストが実行されるようなオプション等は存在しますか?

v0.64.1時点ではありません。ステップの失敗が発生すると即シナリオ失敗となっています。


なお、後者の質問に対してですが、現在オプションの作成を検討しておりそのための実装を追加している最中です。
そうすると必然的に前者の質問(ステップごとの失敗)についても対応していくことになるのではないかと思います。

calloc134calloc134

ご返信ありがとうございます!実装お待ちしています。

Ken’ichiro OyamaKen’ichiro Oyama

ステップが一つ失敗したとき、以降のテストが実行されませんが、以降のテストが実行されるようなオプション等は存在しますか?

こちらは v0.66.0 で force: セクションで実現できるようになりましたので共有しておきます。

Ken’ichiro OyamaKen’ichiro Oyama

一つのシナリオごとにテストの成功/失敗を表示していますが、ステップごとの成功の可否を表示することはできますでしょうか?

こちらは v0.68.0 で --verbose オプションを付与して runn run を実行することで確認できるようになりましたので共有します。

harachanharachan

素敵なツールありがとうございます!!
「Homebrewでインストールする」の内容が brewコマンドを用いたものではなく go installしているものだったので誤植かなと思い報告させていただきます
https://zenn.dev/k1low/books/runn-cookbook/viewer/install#homebrewでインストールする

びきニキびきニキ

はじめまして!1つ質問させてください。
Sessionベースの認証が必要なAPIを突破する方法はありますか?見落としていたらすみません🙇
お手隙のタイミングで教えていただけると幸いです!よろしくお願いします。

Ken’ichiro OyamaKen’ichiro Oyama

Sessionベースといっても一概に1つの実装に限定できるわけではありません。

https://www.php.net/manual/ja/book.session.php

例えば、クライアント側にSession IDを渡すことで認証状態を維持しているのであれば、そのSession IDの取得方法とSession IDの受け渡し方法をrunnで模倣できれば可能だと思います。

PHPマニュアルでもSession IDの受け渡し方法として2つ紹介されています。
https://www.php.net/manual/ja/session.idpassing.php

クライアント側にSession IDを受け渡すことができればいいので、実装方法も、上記2つに限らないでしょう。

例えばPHPアプリケーションだと(何も設定を変えていなければ) PHPSESSID という名前のCookieを経由してSession IDをブラウザ側に保存しているはずです。

https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie

びきニキびきニキ

参考URLまでありがとうございます。仰る通り「クライアント側にSession IDを渡すことで認証状態を維持」という方法で実装しているようなので、runnで模倣する形で実装してみようと思います。ふわふわした内容の質問で失礼しました…!

toganatogana

シナリオベースのテストを試してみたくて runn を利用してみているのですが、自動生成されたファイルで実行エラーとなり原因がわからないので質問させてください。

runn クックブック を参考にランブックを作成して実行しています。
実行時に invalid char escape (10:2306) となってしまうのですが、レスポンスに含まれていてはいけない文字などありますか?

実行した内容としては下記になります。

$ runn -v
runn version 0.80.2
  1. runn new コマンドで --out--and-run オプションを利用して test 付きのランファイルを作成する
$ runn new --and-run --out runbook.yml -- curl http://localhost/api/tasks -H "accept: application/json"
  1. 作成されたファイルを見る
$ cat ./runbook.yml
desc: Generated by `runn new`
runners:
  req: http://localhost
steps:
- req:
    /api/tasks:
      get:
        headers:
          Accept: application/json
        body: null
  test: |
    current.res.status == 200
    && current.res.headers['Access-Control-Allow-Origin'][0] == "*"
    && current.res.headers['Cache-Control'][0] == "no-cache, private"
    && current.res.headers['Content-Type'][0] == "application/json"
    && 'Date' in current.res.headers
    && current.res.headers['Host'][0] == "localhost"
    && current.res.headers['X-Powered-By'][0] == "PHP/8.2.8"
    && current.res.headers['X-Ratelimit-Limit'][0] == "60"
    && current.res.headers['X-Ratelimit-Remaining'][0] == "59"
    && compare(current.res.body, {"data":[{"id":"01h7j21q12erkf1fwna29shhw7","content":"test2","is_completed":true,"created_at":"2023-08-11T10:25:06.082000Z","content_changed_at":"2023-08-11T10:25:06.083000Z","complete_changed_at":"2023-08-11T10:25:06.085000Z"},{"id":"01h7j21q0xwrh65qdwcskh30kn","content":"test4","is_completed":true,"created_at":"2023-08-11T10:25:06.077000Z","content_changed_at":"2023-08-11T10:25:06.078000Z","complete_changed_at":"2023-08-11T10:25:06.081000Z"},{"id":"01h7j21q0r8x1rnqx63gy4mt4w","content":"test2","is_completed":true,"created_at":"2023-08-11T10:25:06.072000Z","content_changed_at":"2023-08-11T10:25:06.073000Z","complete_changed_at":"2023-08-11T10:25:06.076000Z"},{"id":"01h7j21q0kccqz8dyk6y6tmv0q","content":"test4","is_completed":true,"created_at":"2023-08-11T10:25:06.067000Z","content_changed_at":"2023-08-11T10:25:06.068000Z","complete_changed_at":"2023-08-11T10:25:06.071000Z"},{"id":"01h7j21q0fwham459j2y614tp6","content":"test4","is_completed":true,"created_at":"2023-08-11T10:25:06.063000Z","content_changed_at":"2023-08-11T10:25:06.064000Z","complete_changed_at":"2023-08-11T10:25:06.066000Z"},{"id":"01h7j21q0agq9t4kb6zc4w1kff","content":"test1","is_completed":false,"created_at":"2023-08-11T10:25:06.058000Z","content_changed_at":"2023-08-11T10:25:06.060000Z","complete_changed_at":"2023-08-11T10:25:06.061000Z"},{"id":"01h7j21q057934evj87arm9qfn","content":"test4","is_completed":false,"created_at":"2023-08-11T10:25:06.053000Z","content_changed_at":"2023-08-11T10:25:06.055000Z","complete_changed_at":"2023-08-11T10:25:06.056000Z"},{"id":"01h7j21q00atfjtw7dwa0cxdea","content":"test1","is_completed":false,"created_at":"2023-08-11T10:25:06.048000Z","content_changed_at":"2023-08-11T10:25:06.051000Z","complete_changed_at":"2023-08-11T10:25:06.052000Z"},{"id":"01h7j21pzvgdw9njfrcmb3gy2g","content":"test2","is_completed":false,"created_at":"2023-08-11T10:25:06.043000Z","content_changed_at":"2023-08-11T10:25:06.045000Z","complete_changed_at":"2023-08-11T10:25:06.047000Z"},{"id":"01h7j21pzpwccrn0qaekedqwah","content":"test2","is_completed":false,"created_at":"2023-08-11T10:25:06.038000Z","content_changed_at":"2023-08-11T10:25:06.041000Z","complete_changed_at":"2023-08-11T10:25:06.042000Z"}],"links":{"first":null,"last":null,"prev":null,"next":"http:\/\/localhost\/api\/tasks?cursor=eyJpZCI6IjAxaDdqMjFwenB3Y2NybjBxYWVrZWRxd2FoIiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ"},"meta":{"path":"http:\/\/localhost\/api\/tasks","per_page":10,"next_cursor":"eyJpZCI6IjAxaDdqMjFwenB3Y2NybjBxYWVrZWRxd2FoIiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ","prev_cursor":null}})
  1. 実行する
$ runn run ./runbook.yml
F

1) ./runbook.yml 636eb5202577df2773e016cec9ed364eb920d41c
  Failure/Error: test failed on 'Generated by `runn new`'.steps[0]: invalid char escape (10:2306)
   | && compare(current.res.body, {"data":[{"id":"01h7j21q12erkf1fwna29shhw7","content":"test2","is_completed":true,"created_at":"2023-08-11T10:25:06.082000Z","content_changed_at":"2023-08-11T10:25:06.083000Z","complete_changed_at":"2023-08-11T10:25:06.085000Z"},{"id":"01h7j21q0xwrh65qdwcskh30kn","content":"test4","is_completed":true,"created_at":"2023-08-11T10:25:06.077000Z","content_changed_at":"2023-08-11T10:25:06.078000Z","complete_changed_at":"2023-08-11T10:25:06.081000Z"},{"id":"01h7j21q0r8x1rnqx63gy4mt4w","content":"test2","is_completed":true,"created_at":"2023-08-11T10:25:06.072000Z","content_changed_at":"2023-08-11T10:25:06.073000Z","complete_changed_at":"2023-08-11T10:25:06.076000Z"},{"id":"01h7j21q0kccqz8dyk6y6tmv0q","content":"test4","is_completed":true,"created_at":"2023-08-11T10:25:06.067000Z","content_changed_at":"2023-08-11T10:25:06.068000Z","complete_changed_at":"2023-08-11T10:25:06.071000Z"},{"id":"01h7j21q0fwham459j2y614tp6","content":"test4","is_completed":true,"created_at":"2023-08-11T10:25:06.063000Z","content_changed_at":"2023-08-11T10:25:06.064000Z","complete_changed_at":"2023-08-11T10:25:06.066000Z"},{"id":"01h7j21q0agq9t4kb6zc4w1kff","content":"test1","is_completed":false,"created_at":"2023-08-11T10:25:06.058000Z","content_changed_at":"2023-08-11T10:25:06.060000Z","complete_changed_at":"2023-08-11T10:25:06.061000Z"},{"id":"01h7j21q057934evj87arm9qfn","content":"test4","is_completed":false,"created_at":"2023-08-11T10:25:06.053000Z","content_changed_at":"2023-08-11T10:25:06.055000Z","complete_changed_at":"2023-08-11T10:25:06.056000Z"},{"id":"01h7j21q00atfjtw7dwa0cxdea","content":"test1","is_completed":false,"created_at":"2023-08-11T10:25:06.048000Z","content_changed_at":"2023-08-11T10:25:06.051000Z","complete_changed_at":"2023-08-11T10:25:06.052000Z"},{"id":"01h7j21pzvgdw9njfrcmb3gy2g","content":"test2","is_completed":false,"created_at":"2023-08-11T10:25:06.043000Z","content_changed_at":"2023-08-11T10:25:06.045000Z","complete_changed_at":"2023-08-11T10:25:06.047000Z"},{"id":"01h7j21pzpwccrn0qaekedqwah","content":"test2","is_completed":false,"created_at":"2023-08-11T10:25:06.038000Z","content_changed_at":"2023-08-11T10:25:06.041000Z","complete_changed_at":"2023-08-11T10:25:06.042000Z"}],"links":{"first":null,"last":null,"prev":null,"next":"http:\/\/localhost\/api\/tasks?cursor=eyJpZCI6IjAxaDdqMjFwenB3Y2NybjBxYWVrZWRxd2FoIiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ"},"meta":{"path":"http:\/\/localhost\/api\/tasks","per_page":10,"next_cursor":"eyJpZCI6IjAxaDdqMjFwenB3Y2NybjBxYWVrZWRxd2FoIiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ","prev_cursor":null}})
   | .................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................^
  Failure step (./runbook.yml):
   5 - req:
   6     /api/tasks:
   7       get:
   8         headers:
   9           Accept: application/json
  10         body: null
  11   test: |
  12     current.res.status == 200
  13     && current.res.headers['Access-Control-Allow-Origin'][0] == "*"
  14     && current.res.headers['Cache-Control'][0] == "no-cache, private"
  15     && current.res.headers['Content-Type'][0] == "application/json"
  16     && 'Date' in current.res.headers
  17     && current.res.headers['Host'][0] == "localhost"
  18     && current.res.headers['X-Powered-By'][0] == "PHP/8.2.8"
  19     && current.res.headers['X-Ratelimit-Limit'][0] == "60"
  20     && current.res.headers['X-Ratelimit-Remaining'][0] == "59"
  21     && compare(current.res.body, {"data":[{"id":"01h7j21q12erkf1fwna29shhw7","content":"test2","is_completed":true,"created_at":"2023-08-11T10:25:06.082000Z","content_changed_at":"2023-08-11T10:25:06.083000Z","complete_changed_at":"2023-08-11T10:25:06.085000Z"},{"id":"01h7j21q0xwrh65qdwcskh30kn","content":"test4","is_completed":true,"created_at":"2023-08-11T10:25:06.077000Z","content_changed_at":"2023-08-11T10:25:06.078000Z","complete_changed_at":"2023-08-11T10:25:06.081000Z"},{"id":"01h7j21q0r8x1rnqx63gy4mt4w","content":"test2","is_completed":true,"created_at":"2023-08-11T10:25:06.072000Z","content_changed_at":"2023-08-11T10:25:06.073000Z","complete_changed_at":"2023-08-11T10:25:06.076000Z"},{"id":"01h7j21q0kccqz8dyk6y6tmv0q","content":"test4","is_completed":true,"created_at":"2023-08-11T10:25:06.067000Z","content_changed_at":"2023-08-11T10:25:06.068000Z","complete_changed_at":"2023-08-11T10:25:06.071000Z"},{"id":"01h7j21q0fwham459j2y614tp6","content":"test4","is_completed":true,"created_at":"2023-08-11T10:25:06.063000Z","content_changed_at":"2023-08-11T10:25:06.064000Z","complete_changed_at":"2023-08-11T10:25:06.066000Z"},{"id":"01h7j21q0agq9t4kb6zc4w1kff","content":"test1","is_completed":false,"created_at":"2023-08-11T10:25:06.058000Z","content_changed_at":"2023-08-11T10:25:06.060000Z","complete_changed_at":"2023-08-11T10:25:06.061000Z"},{"id":"01h7j21q057934evj87arm9qfn","content":"test4","is_completed":false,"created_at":"2023-08-11T10:25:06.053000Z","content_changed_at":"2023-08-11T10:25:06.055000Z","complete_changed_at":"2023-08-11T10:25:06.056000Z"},{"id":"01h7j21q00atfjtw7dwa0cxdea","content":"test1","is_completed":false,"created_at":"2023-08-11T10:25:06.048000Z","content_changed_at":"2023-08-11T10:25:06.051000Z","complete_changed_at":"2023-08-11T10:25:06.052000Z"},{"id":"01h7j21pzvgdw9njfrcmb3gy2g","content":"test2","is_completed":false,"created_at":"2023-08-11T10:25:06.043000Z","content_changed_at":"2023-08-11T10:25:06.045000Z","complete_changed_at":"2023-08-11T10:25:06.047000Z"},{"id":"01h7j21pzpwccrn0qaekedqwah","content":"test2","is_completed":false,"created_at":"2023-08-11T10:25:06.038000Z","content_changed_at":"2023-08-11T10:25:06.041000Z","complete_changed_at":"2023-08-11T10:25:06.042000Z"}],"links":{"first":null,"last":null,"prev":null,"next":"http:\/\/localhost\/api\/tasks?cursor=eyJpZCI6IjAxaDdqMjFwenB3Y2NybjBxYWVrZWRxd2FoIiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ"},"meta":{"path":"http:\/\/localhost\/api\/tasks","per_page":10,"next_cursor":"eyJpZCI6IjAxaDdqMjFwenB3Y2NybjBxYWVrZWRxd2FoIiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ","prev_cursor":null}})


1 scenario, 0 skipped, 1 failure
Ken’ichiro OyamaKen’ichiro Oyama

http:\/\/localhost\/api\/tasks\/ が原因っぽいです。
runn側でうまく生成orパースの修正で対応ができないか確認します。

toganatogana

確認ありがとうございます。
よろしくお願いします!

toganatogana

ありがとうございます!
実行できるようになりました!

以前と runn new の生成するコードが変わり \// として生成されました。

nittanitta

こんにちは。runnクックブックに載っていたコマンドを実行したところ以下の様なエラーが発生しました。原因がわからないので、教えていただきたいです🙏

$ uname -m
arm64
$ runn -v
runn version 0.91.0
$ curl https://httpbin.org/json -H "accept: application/json"
{
  "slideshow": {
    "author": "Yours Truly",
    "date": "date of publication",
    "slides": [
      {
        "title": "Wake up to WonderWidgets!",
        "type": "all"
      },
      {
        "items": [
          "Why <em>WonderWidgets</em> are great",
          "Who <em>buys</em> WonderWidgets"
        ],
        "title": "Overview",
        "type": "all"
      }
    ],
    "title": "Sample Slide Show"
  }
}
$ runn new --and-run -- curl https://httpbin.org/json -H "accept: application/json"
Error: failed to load runbook /var/folders/7p/g09rrr4n5gjf6c9fwpp5bhjh0000gn/T/runn1459072591/new.yml: scope error: reading files in the parent directory is not allowed. 'read:parent' scope is required: /var/folders/7p/g09rrr4n5gjf6c9fwpp5bhjh0000gn/T/runn1459072591/new.yml
nittanitta

修正確認できました🙇 ご対応ありがとうございます!💪

goemongoemon

runn チュートリアルに記載の内容で include を利用したシナリオを実行したところ、実際の出力が記載の内容と異なっていました。こちらは仕様変更によるものでしょうか?
https://zenn.dev/katzumi/books/runn-tutorial/viewer/include

$ runn --version
runn version 0.99.2
$ USER=katzumi runn run day12/**/*.yml --verbose
=== 既存のシナリオから新しいシナリオを作成する (day12/include.yml)
    --- 指定された件数分、記事一覧を取得します (listArticles) ... ok
    --- 指定された件数分、記事一覧を取得します (listArticles) ... ok
            --- 指定された件数分、記事一覧を取得します (listArticles) ... ok
    --- 1番目の記事の詳細を取得します (showFirstArticle) ... ok
    --- 2番目の記事の詳細を取得します (showSecondArticle) ... ok
=== 単体のシナリオとして定義 (day12/list-articles.yml)
    --- 指定された件数分、記事一覧を取得します (listArticles) ... ok


2 scenarios, 0 skipped, 0 failures
Ken’ichiro OyamaKen’ichiro Oyama

バグ修正しました。報告ありがとうございました!

(なお === の行に ok がないのは仕様変更です)

~/src/github.com/k2tzumi/runn-tutorial (main)> runn --version
runn version 0.99.3
~/src/github.com/k2tzumi/runn-tutorial (main)> env USER=katzumi runn run day12/**/*.yml --verbose
=== 既存のシナリオから新しいシナリオを作成する (day12/include.yml)
    --- 指定された件数分、記事一覧を取得します (listArticles) ... ok
        === 単体のシナリオとして定義 (day12/list-articles.yml)
            --- 指定された件数分、記事一覧を取得します (listArticles) ... ok
    --- 1番目の記事の詳細を取得します (showFirstArticle) ... ok
    --- 2番目の記事の詳細を取得します (showSecondArticle) ... ok
=== 単体のシナリオとして定義 (day12/list-articles.yml)
    --- 指定された件数分、記事一覧を取得します (listArticles) ... ok


2 scenarios, 0 skipped, 0 failures
~/src/github.com/k2tzumi/runn-tutorial (main)>
goemongoemon

バグだったんですね。対応いただきありがとうございます!

taisa831taisa831

runn の test について質問です。

APIのレスポンスが下記のようなjsonの配列形式である場合、type==firstのvalueが1であることをテストしたいです。test: current.res.body.actions[0].value == 1としたりcompareを利用しても配列の順番が毎回変わる場合、actions[0]のtypeがfirstとは限らないためテストができません。

typeがfirstであることをfilterしてvalueが1であることをテストするなどなんらかの形でこのパターンのテストをしたいのですが難しいでしょうか?

{
  "actions": [
    {
      "type": "first",
      "value": 1
    },
    {
      "type": "second",
      "value": 2
    },
    {
      "type": "third",
      "value": 3
    }
  ]
}
Ken’ichiro OyamaKen’ichiro Oyama
desc: Example
vars:
  actions: [
    {
      "type": "first",
      "value": 1
    },
    {
      "type": "second",
      "value": 2
    },
    {
      "type": "third",
      "value": 3
    }
  ]

steps:
  -
    desc: test always second
    test: 'find(vars.actions, {.type == "second"}).value == 2'

こんな感じですかねー。

実行できるランブックは以下です。

https://gist.github.com/k1LoW/bbe1bf4c0d66aeb44304f60a57713f00

$ runn run gist://bbe1bf4c0d66aeb44304f60a57713f00#file-test-yml --scopes read:remote
.

1 scenario, 0 skipped, 0 failures
komeikomei

こんにちは!
runnを便利に使わせていただいております 🙇 ありがとうございます!
includeを利用してrunbookを書いた時の runn run runbook.yml --verbose の挙動についての質問です。

test.yml

desc: "desc test when another runbook included"
steps:
  dumpHoge:
    desc: "dump hoge"
    include:
      path: include.yml
      vars:
        string: hoge

include.yml(include用のrunbook)

desc: "include runbook"
if: included
vars:
  string: default
steps:
  dumpString:
    desc: "dump specified string"
    dump: vars.string

サンプルとして、上記のようにtest.ymlからinclude.ymlをincludeを利用して呼び出して実行しているようなrunbookを記述しました。
このとき、runn run test.yml --verbose を実行した結果が以下になるのですが、include側のステップに定義したdescription(include.yml)で呼び出し元のステップのdescription(test.yml)が上書きされている?ような出力になっている気がします。(想定通りの挙動だったらすいません 🙇)

実行結果:

$ runn --version             
runn version 0.109.0

$ runn run test.yml --verbose
=== desc test when another runbook included (test.yml)
hoge
    --- dump specified string (dumpString) ... ok         <- ここが上書きされてる?
        === include runbook (include.yml)
            --- dump specified string (dumpString) ... ok


1 scenario, 0 skipped, 0 failures

私は以下のような出力となると考えていたので質問させていただきました。

$ runn run test.yml --verbose
=== desc test when runbook included (test.yml)
hoge
    --- dump hoge (dumpHoge) ... ok            <-  test.ymlのステップのdesc
        === include runbook (include.yml)
            --- dump specified string (dumpString) ... ok


1 scenario, 0 skipped, 0 failures
komeikomei

対応ありがとうございました!!!助かります!!!

髙野 将髙野 将

application/jsonレスポンスのbodyに"_"始まりのキーがあると、step.res.bodyにマップされずnullになる

https://x.com/masaru_b_cl/status/1796010645294289372

でお知らせした件です。

再現手順

underscore-test.yaml
desc: アンスコをキーに持つJSONのテスト
runners:
  req: https://gist.githubusercontent.com/
steps:
- req:
    /masaru-b-cl/71f9e92d3a47504e78642e79fd016235/raw/f0f248e49a800b2c992086f67f3f985b5db73a9a/test.json:
      get:
        body:
          application/json: null
  test: |
    current.res.status == 200
- dump: steps[0].res.body
- dump: steps[0].res.rawBody
$ runn run underscore-test.yaml --verbose
=== アンスコをキーに持つJSONのテスト (underscore-test.yaml)
    --- (0) ... ok
null
    --- (1) ... ok
{
  "_hoge": "piyo"
}
    --- (2) ... ok


1 scenario, 0 skipped, 0 failures

取得先JSON

https://gist.github.com/masaru-b-cl/71f9e92d3a47504e78642e79fd016235

test.json
{
  "_hoge": "piyo"
}

期待結果

bodyにマップされ、testコマンドで参照できる

備考

  • Go言語 or runnの仕様上難しい?
    • Go言語仕様上mapのキーは"_"許可されているらしいけど……
    • それをrunnで current.res.body._key みたいに参照させる?
      • current.res.body[_key] みたいになっても多少は仕方ない気もするが
Ken’ichiro OyamaKen’ichiro Oyama

こちらの再現データについてはレスポンスが Content-Type: text/plain; charset=utf-8 であることが原因でした。

$ go run cmd/runn/main.go run tmp.yml --debug
Run "req" on "アンスコをキーに持つJSONのテスト".steps[0]
-----START HTTP REQUEST-----
GET /masaru-b-cl/71f9e92d3a47504e78642e79fd016235/raw/f0f248e49a800b2c992086f67f3f985b5db73a9a/test.json HTTP/1.1
Host: gist.githubusercontent.com
Content-Type: application/json


-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
HTTP/2.0 200 OK
Content-Length: 21
Accept-Ranges: bytes
Access-Control-Allow-Origin: *
Cache-Control: max-age=300
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox
Content-Type: text/plain; charset=utf-8
Cross-Origin-Resource-Policy: cross-origin
Date: Thu, 30 May 2024 12:56:41 GMT
Etag: W/"fe9ae6a411fbb6cad1c24ea0adef516862396cfa4bbf3c7ee20f6d58e1221a35"
Expires: Thu, 30 May 2024 13:01:41 GMT
Source-Age: 190
Strict-Transport-Security: max-age=31536000
Vary: Authorization,Accept-Encoding,Origin
Via: 1.1 varnish
X-Cache: HIT
X-Cache-Hits: 1
X-Content-Type-Options: nosniff
X-Fastly-Request-Id: 805660a4ab04e3973c0c6d7ac3934284565ca6d7
X-Frame-Options: deny
X-Github-Request-Id: A8C8:3EC055:929D4C:AB6F8F:665876C9
X-Served-By: cache-itm1220057-ITM
X-Timer: S1717073801.101739,VS0,VE1
X-Xss-Protection: 1; mode=block

{
  "_hoge": "piyo"
}
-----END HTTP RESPONSE-----
Run "test" on "アンスコをキーに持つJSONのテスト".steps[0]

Run "dump" on "アンスコをキーに持つJSONのテスト".steps[1]
null

Run "dump" on "アンスコをキーに持つJSONのテスト".steps[2]
{
  "_hoge": "piyo"
}
.

1 scenario, 0 skipped, 0 failures
$

https://github.com/k1LoW/runn/blob/7233b172eabca9d311403abd3ff5b8655ae684ce/http.go#L531-L539

強引に条件分岐を変えたところ current.res.body._hoge は取得できるようでした。

~/src/github.com/k1LoW/runn (main)> git diff
diff --git a/http.go b/http.go
index b585969..a0206e3 100644
--- a/http.go
+++ b/http.go
@@ -528,7 +528,7 @@ func (rnr *httpRunner) run(ctx context.Context, r *httpRequest, s *step) error {

        d := map[string]any{}
        d[httpStoreStatusKey] = res.StatusCode
-       if strings.Contains(res.Header.Get("Content-Type"), "json") && len(resBody) > 0 {
+       if strings.Contains(res.Header.Get("Content-Type"), "text") && len(resBody) > 0 {
                var b any
                if err := json.Unmarshal(resBody, &b); err != nil {
                        return err
~/src/github.com/k1LoW/runn (main)> cat tmp.yml
desc: アンスコをキーに持つJSONのテスト
runners:
  req: https://gist.githubusercontent.com/
steps:
- req:
    /masaru-b-cl/71f9e92d3a47504e78642e79fd016235/raw/f0f248e49a800b2c992086f67f3f985b5db73a9a/test.json:
      get:
        body:
          application/json: null
  test: |
    current.res.status == 200
- dump: steps[0].res.body._hoge
- dump: steps[0].res.rawBody
~/src/github.com/k1LoW/runn (main)> go run cmd/runn/main.go run tmp.yml --debug
Run "req" on "アンスコをキーに持つJSONのテスト".steps[0]
-----START HTTP REQUEST-----
GET /masaru-b-cl/71f9e92d3a47504e78642e79fd016235/raw/f0f248e49a800b2c992086f67f3f985b5db73a9a/test.json HTTP/1.1
Host: gist.githubusercontent.com
Content-Type: application/json


-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
HTTP/2.0 200 OK
Content-Length: 21
Accept-Ranges: bytes
Access-Control-Allow-Origin: *
Cache-Control: max-age=300
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox
Content-Type: text/plain; charset=utf-8
Cross-Origin-Resource-Policy: cross-origin
Date: Thu, 30 May 2024 12:59:40 GMT
Etag: W/"fe9ae6a411fbb6cad1c24ea0adef516862396cfa4bbf3c7ee20f6d58e1221a35"
Expires: Thu, 30 May 2024 13:04:40 GMT
Source-Age: 19
Strict-Transport-Security: max-age=31536000
Vary: Authorization,Accept-Encoding,Origin
Via: 1.1 varnish
X-Cache: HIT
X-Cache-Hits: 1
X-Content-Type-Options: nosniff
X-Fastly-Request-Id: 0bd36fd8af0cfdfa547293d2ce3b876985f62ffb
X-Frame-Options: deny
X-Github-Request-Id: A8C8:3EC055:929D4C:AB6F8F:665876C9
X-Served-By: cache-itm1220026-ITM
X-Timer: S1717073980.224917,VS0,VE1
X-Xss-Protection: 1; mode=block

{
  "_hoge": "piyo"
}
-----END HTTP RESPONSE-----
Run "test" on "アンスコをキーに持つJSONのテスト".steps[0]

Run "dump" on "アンスコをキーに持つJSONのテスト".steps[1]
piyo

Run "dump" on "アンスコをキーに持つJSONのテスト".steps[2]
{
  "_hoge": "piyo"
}
.

1 scenario, 0 skipped, 0 failures
~/src/github.com/k1LoW/runn (main)>
髙野 将髙野 将

確認ありがとうございます。

こちらでも runn を git clone して確認して、 Content-Type が "text/plain; charset=utf-8"であることを確認しました。

ということで Content-Type で分岐していることがわかったので、元のHTTPストリーミングでレスポンスを返すAPIのレスポンスヘッダを確認したところ Content-Type: application/json がありませんでした。結果的に step.res.body がnullになっていることがわかりました。

一旦原因はわかりましたので、本スレッドはクローズとします。
また、調査、対応いただきたいことが出てきたら改めてスレッドを作成します。

ご対応、ありがとうございました。

髙野 将髙野 将

社内のエンジニアに共有したところ、 Content-Type: application/json を付与する形に修正して解決しそうです

髙野 将髙野 将

無事解決しました!

runnのコードを私も読んで学びになりました。
今後変な動きっぽいのあったら、自分でrunnのGoコードを動かして試せそうです。

当初(結果的には)誤った情報で混乱させてしまい、お手数をおかけしました。

sadanosadano

シナリオテストの作成に runn を利用させて頂いています。ありがとうございます。

include 利用時の vars のパス解決に関して質問させて下さい。
今作成中のシナリオでは、以下のように、include 専用シナリオに Go Template を使ってリクエストボディを指定しています。

steps:
  generateCode:
    bind:
      resource_code: faker.UUID()
  # リソース作成
  createResource:
    include:
      # API を呼び出す include 専用シナリオを読み込む
      path:
        ../../includes/create_resource.yaml
      # API のリクエストボディは Go Template で指定
      vars:
        request: “json://../../scenarios/case1/request.json.template”
    bind:
      request: current.request

この際、include セクションでは vars を解決する際に path で指定したディレクトリを root とする為、vars には include 先からの相対パスで指定する必要がある認識です。
出来ればシナリオファイル間で vars の値を統一したく、これを、シナリオファイル自体からの相対パス(json://request.json.template)で指定したいのですが、現状何か回避策はございますでしょうか?

Ken’ichiro OyamaKen’ichiro Oyama

現状パスは文字列として渡されるので「ルートシナリオファイルからの相対パス」にする方法はなさそうです。

可能であれば json://request.json.template をルートシナリオで展開し、その vars を include ランナーに渡せればいいのですが難しそうですね。

sadanosadano

ご回答ありがとうございます 🙇
現状では回避が難しい旨、承知しました。
確かに先にリクエスト内容が展開できればそれでも良さそうですね。

また、これは思いつきなのですが、下記の bookWithStore で include 対象のパスだけ渡しているところを、include 対象のパスとは別に operatorRoot のパスを指定可能にするというのはどうでしょうか?
https://github.com/k1LoW/runn/blob/main/include.go#L125

そうすると、例えば以下のような感じで書けるようになりますし、未指定の場合は path の値を使うようにすれば後方互換性も保てるかなと思います。
(ただ、include の vars 以外の処理への影響はあまり考慮できていないです。)

    include:
      root: ./
      path: ../../includes/create_resource.yaml
      vars:
        # root で指定したパスで解決するので、ルートシナリオからの相対パスで記述可能
        request: “json://request.json.template”
Ken’ichiro OyamaKen’ichiro Oyama

fmfm docker/build-push-action の context みたいな立ち位置ですね。

https://github.com/docker/build-push-action/blob/94f8f8c2eec4bc3f1d78c1755580779804cb87b2/action.yml#L40-L42

良いアプローチのように見えますが、一方で sadano さんのおっしゃるように影響範囲の特定が難しいですね。。。(runnは各シナリオファイルの位置をrootにすることで、どこで実行しても、include元のファイルを直接実行しても変わらないようにするという方針をとっています)

別のアプローチとして、例えば、 built-in function に os.Getwd の結果を受け取れる関数を用意するというのはいかがでしょう?
そうすると、シンタックスを増やすことなく、ある程度(あくまである程度ですが)の対応ができるかと思いました。
これもジャストアイデアなので、うまい解決ができると良いと思っています。

sadanosadano

runnは各シナリオファイルの位置をrootにすることで、どこで実行しても、include元のファイルを直接実行しても変わらないようにするという方針をとっています

なるほど。確かにこの方針は変えない方が安全ですね。
下手に入れると今後の機能拡張に影を落としそうな気がします。

別のアプローチとして、例えば、 built-in function に os.Getwd の結果を受け取れる関数を用意するというのはいかがでしょう?

汎用性があって、影響もユーザが意識的に使った範囲に限定されるので、良いアイデアだと思いました!
今回議題に挙げている jsonEvaluator についても、os.Getwd を使って絶対パスで指定すれば問題が解消できそうです。

Ken’ichiro OyamaKen’ichiro Oyama

ああ、${PWD} と使うことで特に関数を提供することなく実現できそうですね...いかがでしょう?

macOS

$ echo $PWD
/Users/k1low
$

Linux

$ docker container run -it --rm --name test ubuntu:latest /bin/bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
eed1663d2238: Pull complete
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
Status: Downloaded newer image for ubuntu:latest
root@94c1abcf645c:/# echo $PWD
/
root@94c1abcf645c:/#
sadanosadano

なるほど、そういう手もあるんですね!
早速 $PWD を変数に bind して以下のように書いてみました。

vars:
    request: “json://{{ vars.pwd }}/request.json.template”

ただ、試してみたところ $PWD は実行時のカレントディレクトリになるため、以下のようにテンプレートまでのパスを書かないとダメでした。
相対パスよりはいくぶん綺麗に見えますが、実行時のカレントディレクトリに依存するのも微妙な気がしたので、まだ相対パスの方が良さそうです。

vars:
    request: “json://{{ vars.pwd }}/test/scenarios/case1/request.json.template”

絶対パスを使う方法で解決しようとすると、ルートシナリオファイル自体の絶対パスが取得できないと難しそうですね。

Ken’ichiro OyamaKen’ichiro Oyama

絶対パスを使う方法で解決しようとすると、ルートシナリオファイル自体の絶対パスが取得できないと難しそう

確かにですねえ。。

sadanosadano

ただ、現状の相対パスでの記述でも問題なくテスト実装出来ており、非常に便利に利用させて頂いています!
何か妙案が出て解消できそうであれば、是非検討して頂けるとありがたいです 🙇

asayamakkasayamakk

こんにちは!シナリオテストを導入していきたくて、まさにrunnがいいなと思って使い始めました!
HTTP APIのテストを書いており、HTTPリクエストのデフォルト設定に関して質問させてください!

常につける必要があるリクエストヘッダーがあるため、reqが出てくる箇所で全て書くのではなく、
共通のリクエスト定義をどこかに書いておいて、各シナリオではそれを暗黙的に使う
みたいな形で呼び出したいと思っており、そういったことは実現できますか?

https://github.com/k1LoW/runn/blob/main/testdata/book/custom_runner_default_header.yml

を見ていると、それっぽいことができそうな雰囲気を感じてはいるのですが、
具体的にどうかけばよいのだ・・・? と悩んで手が止まってしまっております..!

  • endpointは環境変数で指定できるようにする
  • リクエストヘッダに、 X-Custom-Value: 1 のようなヘッダをつけたい

がやりたいことです! 最高のツール、ありがとうございます!!

Ken’ichiro OyamaKen’ichiro Oyama

asayamakk さんが貼っているtestdata内のランブックの通り、現状カスタムランナー("Custom runner")という機能を使って実現できます。

custom_runner_default_header.yml で定義したカスタムランナーを

https://github.com/k1LoW/runn/blob/c26488bffad74b50a1db690944dbb472c1a6fdbf/testdata/book/custom_runners.yml#L3-L8

のようにランナーとして登録します(この場合は default というキーでランナーを登録している)。

使い方は定義次第ですが、 custom_runner_default_header.yml の場合は次のようになります。

https://github.com/k1LoW/runn/blob/c26488bffad74b50a1db690944dbb472c1a6fdbf/testdata/book/custom_runners.yml#L17-L27

ただ、カスタムランナー機能は現在Experimentalな、というよりアルファ版な位置付けです。
そのためREADMEにも書いていません。
私自身も業務で使っている機能ではあるのですが、その点注意して使っていただけると幸いです。

asayamakkasayamakk

ありがとうございます!

色々と試行錯誤してみましたが上手くいかず、まだまだrunn初学者なため
YAMLの参照を用いる + 各ファイルに1度だけ共通ヘッダーの定義を書く
という形に落ち着くことにしました!

desc: 患者作成→予約作成→カルテ記入→会計保存の一連を実行する
runners:
  req:
    endpoint: ${RUNN_HOST-https://example.com}
    headers: &defaultHeaders
      X-Custom-Value: "1"
  getUsers:
    desc: get users
    req:
      /api/users:
        get:
          headers: *defaultHeaders

もっと使い込んでみて、このまま状態だと解決できない課題が出てきた際に
カスタムランナーの利用を再検討しようと思います ☕

Ken’ichiro OyamaKen’ichiro Oyama

うまいやり方ですね!
1つのランブックに閉じたやり方でよければ vars: セクションにセットした値を使うのも1つの手かもです。

ところで

 desc: 患者作成→予約作成→カルテ記入→会計保存の一連を実行する
 runners:
   req:
     endpoint: ${RUNN_HOST-https://example.com}
     headers: &defaultHeaders
       X-Custom-Value: "1"
+steps:
   getUsers:
     desc: get users
     req:
       /api/users:
         get:
           headers: *defaultHeaders

ですかね

髙野 将髙野 将

負荷試験にrunnを利用していて、疑問点が出てきたので確認させてください

  • 何らかのオプション指定で解決できるならその方法が知りたい
  • できないなら runn の改修で対応可能なのか

起きたこと

durationを過ぎると、最後に実行していたシナリオ実行が打ち切られて必ず失敗扱いになる

影響

100並列で実行するようなテスト実施時、少なくない数の最終シナリオ実行が失敗になるため、負荷試験結果でその分を差し引いて判断が必要になってしまう

再現手順

1. 5秒待ってレスポンスを返すAPIを作成する

例)Next.js App Routes

/api/hello.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";

type Data = {
  name: string;
};

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>,
) {
  // 10秒待機
  setTimeout(() => {
    res.status(200).json({ name: "John Doe" });
  }, 5000);
}

2. 1.のAPIを実行するrunbookを作成する

call-hello.yaml
desc: テスト
runners:
  req: http://localhost:3000
steps:
  - req:
      /api/hello:
        get:
          body:
            application/json:
              null
    test: |
      current.res.status == 200

実行確認結果

$ runn run call-hello.yaml --debug
Run "req" on "テスト".steps[0]
-----START HTTP REQUEST-----
GET /api/hello HTTP/1.1
Host: localhost:3000
Content-Type: application/json


-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
HTTP/1.1 200 OK
Content-Length: 19
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Date: Tue, 03 Sep 2024 01:58:58 GMT
Etag: "wjzyexk8pqj"
Keep-Alive: timeout=5
Vary: Accept-Encoding

{"name":"John Doe"}
-----END HTTP RESPONSE-----
Run "test" on "テスト".steps[0]
.

1 scenario, 0 skipped, 0 failures

3. loadt コマンドでdurationを1秒にして実行する

$ runn loadt --warm-up=0s --duration=1s call-hello.yaml --debug
Run "req" on "テスト".steps[0]
                              -----START HTTP REQUEST-----
                                                          GET /api/hello HTTP/1.1
Host: localhost:3000
Content-Type: application/json


-----END HTTP REQUEST-----

Number of runbooks per RunN....: 1
Warm up time (--warm-up).......: 0s
Duration (--duration)..........: 1s
Concurrent (--load-concurrent).: 1
Max RunN per second (--max-rps): 1

Total..........................: 1
Succeeded......................: 0
Failed.........................: 1
Error rate.....................: 100%
RunN per second................: 1
Latency .......................: max=1.0ms min=1.0ms avg=1.0ms med=1.0ms p(90)=1.0ms p(99)=1.0ms

期待する結果

durationを過ぎても打ち切られず、1回分の結果が採取され、レポーティングされる

例)レポート例

Number of runbooks per RunN....: 1
Warm up time (--warm-up).......: 0s
Duration (--duration)..........: 1s
Concurrent (--load-concurrent).: 1
Max RunN per second (--max-rps): 1

Total..........................: 1
Succeeded......................: 1
Failed.........................: 0
Error rate.....................: 0%
RunN per second................: 0.2
Latency .......................: max=5s min=5s avg=5s med=5s p(90)=5s p(99)=5s
  • Succeeded/Failed/Error rate: 成功扱いとしてカウント
  • RunN per second: 実際に掛かった時間(5s程度)から逆算
  • Latency: 実際の結果の5秒程度

参考

k6では最後まで実行されましたので、同じような結果を期待します

シナリオ

call-hello.js
import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  vus: 1,        // 仮想ユーザー数を1に設定(必要に応じて調整可能)
  stages: [      // ウォームアップなしのためにstagesは使用しない
    { duration: '1s', target: 1 },
  ],
  noConnectionReuse: true, // コネクションの再利用を無効にして、よりクリーンなテストを実施
};

export default function () {
  http.get('http://localhost:3000/api/hello');
}

k6実行結果

$ k6 run call-hello.js

          /\      |‾‾| /‾‾/   /‾‾/   
     /\  /  \     |  |/  /   /  /    
    /  \/    \    |     (   /   ‾‾\  
   /          \   |  |\  \ |  (‾)  | 
  / __________ \  |__| \__\ \_____/ .io

     execution: local
        script: call-hello.js
        output: -

     scenarios: (100.00%) 1 scenario, 1 max VUs, 31s max duration (incl. graceful stop):
              * default: 1 looping VUs for 1s (gracefulStop: 30s)


     data_received..................: 233 B 46 B/s
     data_sent......................: 89 B  18 B/s
     http_req_blocked...............: avg=1.53ms min=1.53ms med=1.53ms max=1.53ms p(90)=1.53ms p(95)=1.53ms
     http_req_connecting............: avg=274µs  min=274µs  med=274µs  max=274µs  p(90)=274µs  p(95)=274µs 
     http_req_duration..............: avg=5.01s  min=5.01s  med=5.01s  max=5.01s  p(90)=5.01s  p(95)=5.01s 
       { expected_response:true }...: avg=5.01s  min=5.01s  med=5.01s  max=5.01s  p(90)=5.01s  p(95)=5.01s 
     http_req_failed................: 0.00% ✓ 0        ✗ 1  
     http_req_receiving.............: avg=163µs  min=163µs  med=163µs  max=163µs  p(90)=163µs  p(95)=163µs 
     http_req_sending...............: avg=186µs  min=186µs  med=186µs  max=186µs  p(90)=186µs  p(95)=186µs 
     http_req_tls_handshaking.......: avg=0s     min=0s     med=0s     max=0s     p(90)=0s     p(95)=0s    
     http_req_waiting...............: avg=5.01s  min=5.01s  med=5.01s  max=5.01s  p(90)=5.01s  p(95)=5.01s 
     http_reqs......................: 1     0.199307/s
     iteration_duration.............: avg=5.01s  min=5.01s  med=5.01s  max=5.01s  p(90)=5.01s  p(95)=5.01s 
     iterations.....................: 1     0.199307/s
     vus............................: 1     min=1      max=1
     vus_max........................: 1     min=1      max=1


running (05.0s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs  1s
Ken’ichiro OyamaKen’ichiro Oyama

ありがとうございますー。こちら修正中です。
基本的には実行開始したシナリオグループについてはduration中に滑り込んだリクエストについては最後まで完了を待つようにしようと思います。

RunN per second: 実際に掛かった時間(5s程度)から逆算

これに関しては、滑り込み分による実行時間超過は計測せずに従来通り計測しようと思っています。

理由としては、duration 1分指定での計測をしてRPSを出す場合、「1分の指定で何回リクエストを(開始)できたかをRPS」と判断したからです。

髙野 将髙野 将

brew update && brew upgrade してもまだ取れなかったので、リリースページからダウンロードしたバイナリを直接実行してみました。

$ ./runn loadt --warm-up=0s --duration=1s call-hello.yaml --debug
Run "req" on "テスト".steps[0]
                              -----START HTTP REQUEST-----
                                                          GET /api/hello HTTP/1.1
Host: localhost:3000
Content-Type: application/json


-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
                             HTTP/1.1 200 OK
Content-Length: 27
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Date: Fri, 06 Sep 2024 04:09:08 GMT
Etag: "lujfcqqq7gr"
Keep-Alive: timeout=5
Vary: Accept-Encoding

{"message":"Hello, world!"}
                           -----END HTTP RESPONSE-----
                                                      Run "test" on "テスト".steps[0]

Number of runbooks per RunN....: 1
Warm up time (--warm-up).......: 0s
Duration (--duration)..........: 1s
Concurrent (--load-concurrent).: 1
Max RunN per second (--max-rps): 1

Total..........................: 1
Succeeded......................: 0
Failed.........................: 1
Error rate.....................: 100%
RunN per second................: 1
Latency .......................: max=5,108.8ms min=5,108.8ms avg=5,108.8ms med=5,108.8ms p(90)=5,108.8ms p(99)=5,108.8ms

対応方針通りに修正されていそうなことを確認しました。
非常に助かります。ありがとうございました!

髙野 将髙野 将

「修正されていそう」ってコメントしたのですが、「duration内にレスポンスが返ってこなかったリクエスト」の結果は Failed 扱いになっているんですね。

↓の部分

Failed.........................: 1
Error rate.....................: 100%

待っても待たなくても「Failed」扱いになるのであれば、待つ必要性はなくなってしまうと感じましたが、いかがでしょう?

Ken’ichiro OyamaKen’ichiro Oyama

手元で同様のNext Appを作成して試してみたところうまくいっているようです。
今のところ再現できておりません。

再現方法が分かりましたら教えていただけると嬉しいです。

~/src/github.com/k1LoW/runn (main)> runn -v
runn version 0.119.0
~/src/github.com/k1LoW/runn (main)> cat call-hello.yaml
desc: テスト
runners:
  req: http://localhost:3000
steps:
  - req:
      /api/hello:
        get:
          body:
            application/json:
              null
    test: |
      current.res.status == 200%
~/src/github.com/k1LoW/runn (main)> runn loadt --warm-up=0s --duration=1s call-hello.yaml --debug
Run "req" on "テスト".steps[0]
                              -----START HTTP REQUEST-----
                                                          GET /api/hello HTTP/1.1
Host: localhost:3000
Content-Type: application/json


-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
                             HTTP/1.1 200 OK
Content-Length: 19
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Date: Fri, 06 Sep 2024 08:29:19 GMT
Etag: "wjzyexk8pqj"
Keep-Alive: timeout=5
Vary: Accept-Encoding

{"name":"John Doe"}
                   -----END HTTP RESPONSE-----
                                              Run "test" on "テスト".steps[0]

Number of runbooks per RunN....: 1
Warm up time (--warm-up).......: 0s
Duration (--duration)..........: 1s
Concurrent (--load-concurrent).: 1
Max RunN per second (--max-rps): 1

Total..........................: 1
Succeeded......................: 1
Failed.........................: 0
Error rate.....................: 0%
RunN per second................: 1
Latency .......................: max=5,026.1ms min=5,026.1ms avg=5,026.1ms med=5,026.1ms p(90)=5,026.1ms p(99)=5,026.1ms

~/src/github.com/k1LoW/runn (main)>
~/src/github.com/k1LoW/runn-test/my-app (main)> npm run dev

> my-app@0.1.0 dev
> next dev

  ▲ Next.js 14.2.8
  - Local:        http://localhost:3000

 ✓ Starting...
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry

 ✓ Ready in 1741ms
 ✓ Compiled /api/hello in 162ms (66 modules)
API resolved without sending a response for /api/hello, this may result in stalled requests.
 GET /api/hello 200 in 5185ms
API resolved without sending a response for /api/hello, this may result in stalled requests.
 GET /api/hello 200 in 5006ms
API resolved without sending a response for /api/hello, this may result in stalled requests.
 GET /api/hello 200 in 5005ms
API resolved without sending a response for /api/hello, this may result in stalled requests.
 GET /api/hello 200 in 5009ms
API resolved without sending a response for /api/hello, this may result in stalled requests.
 GET /api/hello 200 in 5009ms
API resolved without sending a response for /api/hello, this may result in stalled requests.
 GET /api/hello 200 in 5007ms
API resolved without sending a response for /api/hello, this may result in stalled requests.
 GET /api/hello 200 in 5004ms
API resolved without sending a response for /api/hello, this may result in stalled requests.
 GET /api/hello 200 in 5004ms
髙野 将髙野 将

改めて、まっさらなNext.jsアプリ作って試したのですが再現しますね……なんだろう……

記録残しておきます

node、npmバージョン

$ node -v
v20.17.0
$ npm -v
10.8.2

行ったこと

Nexs.jsアプリ新規作成

最初の✔ Would you like to use TypeScript?Yes、その他はすべてNo

$ npx create-next-app@latest
✔ What is your project named? … hello
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Creating a new Next.js app in /Users/takano.sho/sandbox/next-practice/a/hello.

Using npm.

Initializing project with template: default 


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom


added 28 packages, and audited 29 packages in 4s

3 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Initialized a git repository.

Success! Created hello at /path/to/hello

作成されたプロジェクトのパッケージ構成

package.json
{
  "name": "hello",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "react": "^18",
    "react-dom": "^18",
    "next": "14.2.8"
  },
  "devDependencies": {
    "typescript": "^5",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18"
  }
}

pages/api/hello.tsを書き換え

5行待つように

hello.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";

type Data = {
  name: string;
};

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>,
) {
  // 5秒待機
  setTimeout(() => {
    res.status(200).json({ name: "John Doe" });
  }, 5000);
}

Next.js アプリ実行、runn loadt実行

runn loadt実行結果

$ runn -v
runn version 0.119.0

$ runn loadt --warm-up=0s --duration=1s call-hello.yaml --debug
Run "req" on "テスト".steps[0]
                              -----START HTTP REQUEST-----
                                                          GET /api/hello HTTP/1.1
Host: localhost:3000
Content-Type: application/json


-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
                             HTTP/1.1 200 OK
Content-Length: 19
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Date: Mon, 09 Sep 2024 01:02:51 GMT
Etag: "wjzyexk8pqj"
Keep-Alive: timeout=5
Vary: Accept-Encoding

{"name":"John Doe"}
                   -----END HTTP RESPONSE-----
                                              Run "test" on "テスト".steps[0]

Number of runbooks per RunN....: 1
Warm up time (--warm-up).......: 0s
Duration (--duration)..........: 1s
Concurrent (--load-concurrent).: 1
Max RunN per second (--max-rps): 1

Total..........................: 1
Succeeded......................: 0
Failed.........................: 1
Error rate.....................: 100%
RunN per second................: 1
Latency .......................: max=5,193.5ms min=5,193.5ms avg=5,193.5ms med=5,193.5ms p(90)=5,193.5ms p(99)=5,193.5ms

npmのコンソール

$ npm run dev        

> hello@0.1.0 dev
> next dev

  ▲ Next.js 14.2.8
  - Local:        http://localhost:3000

 ✓ Starting...
 ✓ Ready in 1123ms
 ✓ Compiled /api/hello in 128ms (66 modules)
API resolved without sending a response for /api/hello, this may result in stalled requests.
 GET /api/hello 200 in 5163ms
髙野 将髙野 将

原因わかりました!

「runnシナリオファイルを変えたら待ち合わせするようになるかもなー」と思って、以下のように「必ず失敗する」シナリオに手元で変えていたのを失念していたためでした(ごめんなさい……

    test: |
      current.res.status == 600

ここを直したら、ちゃんと動くことが確認できました!

$ runn loadt --warm-up=0s --duration=1s call-hello.yaml --debug
Run "req" on "テスト".steps[0]
                              -----START HTTP REQUEST-----
                                                          GET /api/hello HTTP/1.1
Host: localhost:3000
Content-Type: application/json


-----END HTTP REQUEST-----
-----START HTTP RESPONSE-----
                             HTTP/1.1 200 OK
Content-Length: 19
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Date: Mon, 09 Sep 2024 01:27:08 GMT
Etag: "wjzyexk8pqj"
Keep-Alive: timeout=5
Vary: Accept-Encoding

{"name":"John Doe"}
                   -----END HTTP RESPONSE-----
                                              Run "test" on "テスト".steps[0]

Number of runbooks per RunN....: 1
Warm up time (--warm-up).......: 0s
Duration (--duration)..........: 1s
Concurrent (--load-concurrent).: 1
Max RunN per second (--max-rps): 1

Total..........................: 1
Succeeded......................: 1
Failed.........................: 0
Error rate.....................: 0%
RunN per second................: 1
Latency .......................: max=5,149.6ms min=5,149.6ms avg=5,149.6ms med=5,149.6ms p(90)=5,149.6ms p(99)=5,149.6ms

ご対応ありがとうございました!

髙野 将髙野 将

もう一つあります

起きたこと

loadt コマンドのレポートの Latency の値がおかしい

再現手順

上記コメントと同様に1sのdurationで実行

$ runn loadt --warm-up=0s --duration=1s call-hello.yaml --debug
Run "req" on "テスト".steps[0]
                              -----START HTTP REQUEST-----
                                                          GET /api/hello HTTP/1.1
Host: localhost:3000
Content-Type: application/json


-----END HTTP REQUEST-----

Number of runbooks per RunN....: 1
Warm up time (--warm-up).......: 0s
Duration (--duration)..........: 1s
Concurrent (--load-concurrent).: 1
Max RunN per second (--max-rps): 1

Total..........................: 1
Succeeded......................: 0
Failed.........................: 1
Error rate.....................: 100%
RunN per second................: 1
Latency .......................: max=1.0ms min=1.0ms avg=1.0ms med=1.0ms p(90)=1.0ms p(99)=1.0ms
  • durationが1sなので、Latencyの値が「1.0ms」でなく「1,000ms」でないとおかしい

おそらくの原因

https://github.com/k1LoW/runn/blob/main/loadt.go#L119-L124

の箇所で *loadtResult のフィールドの値を参照しているが、 otchkissLatency を記録する箇所は second で登録している

https://github.com/ryo-yamaoka/otchkiss/blob/develop/otchkiss.go#L140

よって、CheckThreshold funcで行っているように、latencyの値に「*1000」が必要に見える

https://github.com/k1LoW/runn/blob/main/loadt.go#L142-L147

その証拠に、 otchkiss のレポート処理では *1000 している

https://github.com/ryo-yamaoka/otchkiss/blob/develop/otchkiss.go#L251-L256

takuuumtakuuum

ブック購入させていただきました。あるアプリケーション(250万MAUくらいの規模)への導入も進めています。
現在、runnでcsvレスポンスの検証ができないかを調査中であり、bind、dump、execあたりをうまく使いながらできないか模索しているところです。
もしrunn開発者さんの中で何かアイデアがありましたら、ご教示いただけると嬉しいです🙇

Ken’ichiro OyamaKen’ichiro Oyama

ブック購入させていただきました。あるアプリケーション(250万MAUくらいの規模)への導入も進めています。

ありがとうございます!

csvレスポンスの検証

HTTPレスポンスが Content-Type: text/csv でCSVが返ってくる感じですかね?

どうするのがいいんだろう...単純にレスポンスボディの文字列比較が簡単そうではありますが、どうでしょう?
runnのHTTPランナーだと current.res.rawBody にそのままのレスポンスボディが入っています。

takuuumtakuuum

HTTPレスポンスが Content-Type: text/csv でCSVが返ってくる感じですかね?

はい!

どうするのがいいんだろう...単純にレスポンスボディの文字列比較が簡単そうではありますが、どうでしょう?

確かに。それが一番簡単そうですね。
試してみます。(また進捗共有させてもらいます)

sadanosadano

runn で負荷試験を実施した際に気になった挙動がありましたので、共有いたします。

負荷試験実行時に、誤って load-concurrent を 0 に指定して実行してしまったのですが、しばらく待った後に結果は表示されずに、以下のようなメッセージが表示されました。

$ runn loadt --load-concurrent 0 --duration 10s --max-rps 0 --scopes run:exec ./test/performancetest/get/scenario.yaml
runtime: failed to create new OS thread

バージョンは 0.119.0 を利用しており、端末のチップは Apple M1 です。

$ runn -v
runn version 0.119.0

また、load-concurrent を 1 以上で指定すると、特に問題なく実行が完了します。

$runn loadt --load-concurrent 10 --duration 10s --max-rps 0 --scopes run:exec ./test/performancetest/get/scenario.yaml

Number of runbooks per RunN....: 1
Warm up time (--warm-up).......: 5s
Duration (--duration)..........: 10s
Concurrent (--load-concurrent).: 10
Max RunN per second (--max-rps): 0

Total..........................: 2367
Succeeded......................: 2367
Failed.........................: 0
Error rate.....................: 0%
RunN per second................: 236.7
Latency .......................: max=1,033.1ms min=28.7ms avg=42.2ms med=38.6ms p(90)=46.6ms p(99)=104.9ms

以上、お手数おかけしますが、ご確認お願いいたします。

Ken’ichiro OyamaKen’ichiro Oyama

--load-concurrent 0 にすると無制限に並列化しようとします。その結果、最終的にruntimeがエラーになったのだと思われます(何かしらのリソース枯渇が原因だと思われます)。

手元で簡単なテストであればかなりの高負荷になりつつも問題なく実行できるようです。
エラーが発生してしまう可能性があるのはよろしくはないですが、限界までの負荷テストをできるように一応残しておこうと思います。

ハトンハトン

いつも便利に使わせてもらっております!負荷試験について質問です。

run loadの実行時に前処理として、指定した並列実行数(concurrent)分だけ web session と XSRF_TOKEN を事前に pool し、web session による保護が必要な API のみを負荷対象として実行する仕組みは存在しますでしょうか?

たとえば、負荷時間が10秒で並列数が50だとした場合、前処理で web session を50個発行します。そして、この10秒間、前処理で取得した web session の50個をそれぞれ並列実行時に独立してAPI callに使用します。

Ken’ichiro OyamaKen’ichiro Oyama

なるほどー。「前処理」は現時点では設定できないですね。。

例えば、「web session を50個」した値をJSONファイルなどに書き出してもらって、それをそれぞれでで使用する「ような機能」というのは機能があっても良いかなと思いました(当然今は機能が足りないのできません。)

なお、50並行だったとして、並行実行している「50個の何かがそれぞれの web session を固定で持つ」というのはGoの並行実行のと実装の仕組み上難しそうです。

ハトンハトン

ご確認ありがとうございます!

質問しておきながらですが私もweb sessionをそれぞれ固定で持って並行実行するのは難しいと感じたので、ご提案していただような方針でGoからrunnをそれぞれのsession毎に呼び出して負荷をかけていくようにしました。

自前実装しなくてもできるようになると、保護されたAPIの負荷試験やれる人が増えて素敵かなと思いますので、もし機会があればご検討頂けると嬉しいです!

Ken’ichiro OyamaKen’ichiro Oyama

runn.i というパラメータを新設して、 runn loadt 実行時にシナリオグループごとにインクリメントされた数値を取得できるようにしました。

前処理でsession情報を事前に用意する必要はありますが、それ以降は runn loadt で適切に使用できるようにできると思います。

https://zenn.dev/k1low/books/runn-cookbook/viewer/load-testing-with-different-params

例では vars.token にしていますが、 json:// でJSONファイルを読み込む形式でも可能です。

ハトンハトン

便利なパラメータありがとうございます!早速負荷試験にこの方式でやってみます。