若くない何かの悩み

何かをdisっているときは、たいていツンデレですので大目に見てやってください。

高熱後の味覚障害時に食べたもの味覚の記録(随時更新)

2023/08/8から08/12にかけて一家全員(イヌ除く)が40度近い発熱をしました。08/13現在、私は味覚を甘味以外(苦味・酸味・旨味・塩味)感じません。嗅覚はほぼ感じません。この状況で飲食をした際に感じたことの記録を残しておきます。

記録

味覚障害 1日目?

カロリーメイトゼリーりんご味の味がりんごのそれにかなり忠実に感じた。ホエータンパクが入っているからヨーグルトのような風味を感じるはずだが感じなかった。完全にりんご味だ!!!と感動したのを覚えている。ただりんご味と感じるには酸味を感じないといけないはずなのでこの頃はまだ酸味を感じていたのかもしれない。

味覚障害 2日目

カロリーメイトゼリーのりんご感が失われた。酸味を感じなくなった。ただ甘いゼリーだったが美味しかった。

レンジだけで麻婆豆腐を作ったら(当時は長引く高熱のせいで破滅願望があった)、辛い熱い以外感じず美味しく感じられなかった。 味覚に障害のなかった妻は美味しい、ちゃんとした麻婆豆腐味だといって食べていた。

味覚障害3日目

カボチャとにんじんのポタージュの作り置きを食べた。塩味を感じなかったので緩いマッシュポテトのように感じられた。

味覚障害4日目

破滅願望が再び高まったので辛ラーメンを食べた。辛味は感じた。塩味をほとんど感じなかったが麻婆豆腐のときほど不味くは感じなかった。もしかすると旨味を感じ始めている…?

味覚障害5日目

コーヒーの苦味を感じない。

味覚障害6日目

カロリーメイトゼリーりんご味のりんご風味がわかるようになった!!!

続く

加筆予定。

FAQ

なんの感染症だったのか?

検査をしていないので正確なところはわかりません。

40度近い高熱が出たことからインフルエンザウイルスか新型コロナウイルスのいずれかに感染したのではないかと推測しています。 当時の在住地域の定点観測の感染者数割合ではインフル感染者1に対して新型コロナ感染者が10のようですから、新型コロナと考えた方が自然かもしれません。

感染の心当たりは?

潜伏期間を3-5日程度と考えると近所の夏祭りに2-3hほど家族で滞在していたときが唯一の心当たりです。 屋外であったこと、熱中症リスクを重くみたことの2点からマスクを所持していたものの着用していませんでした。 また出店の食品を口にしました。

全員に共通して発症日がほぼずれていないこと、症状は鼻ではなく喉から出たことの2点から出店からの食品・食器から感染した疑いもあります。 全員共通して口にしたのはゼリーとかき氷とフランクフルトです。

その後の対応予定は?

夏祭りの運営に感染を報告しようと思っています(多分報告しないと感染者を出さずに終わった!みたいになって来年対策できませんよね…)。 かつ新型コロナ感染症またはインフルエンザである可能性を考慮してそれぞれの外出自粛期間の長い方を採用することにしています。

中古ネットワーク機器で格安インターネット環境を作る

家のネットワーク環境をあれこれ検討していたところ、中古ネットワーク機器のコストパフォーマンスいいよと聞いて中古ネットワーク機器で揃えてみることにしました。

ネットワーク構成図

我が家のネットワーク構成図

インターネット回線

フレッツ光クロスを契約できたのでチャレンジしてみました。

ISP

ぷらら光メイト(光クロス)です。当初は ASAHI ネットの予定だったのですが、レンタルルータのオプションが必須ということでやめました。 ぷららは IPoE 方式が OCN バーチャルコネクトなので色々と問題があるのですが、安さに負けました。

ルーター

NEC UNIVERGE IX2215

情報分電盤が宅内の果てにあるので、ここに無線ルーターを置くと通信の弱い場所ができてしまいます。そこで有線ルーターを置くことにしました。同僚から NEC UNIVERGE IX2215 いいぞ、とうかがったのと、その方も IX2215 へ乗り換えを検討しているとのことだったのでヤフオクで2台セットのものを共同で購入しました。 最大2Gbps のIPv4基本性能 1000BASE-T (訂正: 2023/03/14)とのことで光クロスの性能を使いきれないのですが、中古で送料込み ¥6,000 ほどで手に入る価格が魅力です。大きさは A4 と同じなので A4 の紙で設置イメージをつかめます。

auctions.yahoo.co.jp

UNIVERGE IX2215 : UNIVERGE IXシリーズ | NEC

シリアルコンソールケーブル

設定にはシリアルコンソールケーブルが必要になります。シリアルコンソールケーブルは FTDI のチップのが問題が少なそうなのでチップを基準に選びました。

設定

NEC のサイトに UNIVERGE の設定例が載っているのですが落とし穴がありました。ぷらら光メイト(光クロス)は OCN バーチャルコネクトを利用していて(サポートに問い合わせて確認をとりました)、光クロスには執筆現在(2023/03/13)ひかり電話をつけられないので OCN バーチャルコネクトの動的 IP、ひかり電話なしの設定を参考にするのが正しいように思えますがこれでは接続できません。

UNIVERGE で OCN バーチャルコネクトの接続状況は show map-e status でみられます。ONU と接続後からしばらく経ってもこのコマンドで表示される status が waiting だったため何かおかしいのがわかり、map-e のデバッグログを確認しました。確認のためにログを有効化する必要があります。コンフィグモードに入り、logging を有効化しつつ map-e 関連のログだけログレベルを debug へ変えます:

IX2215# enable-config
IX2215(config)# logging buffered
IX2215(config)# logging subsystem mape debug

これでログが記録されるようになったので show logging でログを確認します:

IX2215(config)# show logging
...

すると IP が取れていないことがわかりました。NEC の設定ガイドにある OCNバーチャルコネクトの動的IP、ひかり電話なしの WAN 側の IP 取得の設定を抜粋します:

ipv6 dhcp client-profile dhcpv6-cl
  information-request
  option-request dns-servers

! ...

interface GigaEthernet0.0
  no ip address
  ipv6 enable
  ipv6 dhcp client dhcpv6-cl
  ipv6 nd proxy GigaEthernet1.0
  no shutdown

Router Advertisement で IPv6 アドレスをもらう設定になっているのですが、どうやらこれだと IP をもらえないようです。サポートに IP もらえないんだけどと連絡しても「ルーターのことはわからん、対応ルーターなら配線するだけで繋がるはず」と言われてしまいます。技術情報をまったく教えてもらえなかったのでダメもとで色々試したところ、DHCPv6-PD だと IP をもらえることがわかりました(追記2023/03/14: 「フレッツ 光クロス」は、ルーターをレンタルしなくてもサービスを利用できますか。 で DHCPv6-PD と明記されていました):

ipv6 dhcp client-profile dhcpv6-cl
  option-request dns-servers
  ia-pd subscriber GigaEthernet1.0 ::/64 eui-64

! ...

interface GigaEthernet0.0
  no ip address
  ipv6 enable
  ipv6 dhcp client dhcpv6-cl
  no shutdown

つまり、どうやらぷらら光メイト(光クロス)は DHCPv6-PD で IP の払い出しをしているようなのです。ここで半日ほど溶かしました。

うまく接続できた設定を貼っておきます。

gist.github.com

宅内配線

CAT6A の配線です。お高かった。。。

スイッチ

引き渡し当初から設置されていたスイッチです。Buffalo LSW6-GT-8NS なのですが、こいつ 1Gbps までしか出せないんですよね。。。せっかく UNIVERGE が 2Gbps 出せるのにもったいない。。。ということでいつかこいつを置き換えます。速度が必要なケーブルだけ IX2215 の GE2 に繋ぐのが ¥0 でできるので有力です。 1000BASE-T (訂正: 2023/03/14)なので IX2215 ではなく IX2310 あたりが安く流れてくるようになったら置き換えるかな〜という感じです。

LSW6-GT-8NS/BK : スイッチングハブ | バッファロー

アクセスポイント

Cisco WAP150

前述の通りルーターに無線機能を持たせない方針なので、アクセスポイントを1Fと2Fの中心に設置することにしました。こちらも中古で手に入るものを探したところ、ヤフオクで電源アダプタつき(重要)の Cisco WAP150 が2台で送料込み ¥8,000 とのことだったのでこれを落札しました。

auctions.yahoo.co.jp

802.11ac wave 1 に対応しているのでそこそこの速度が出ます(wave2 だともっと速度が出るんですけどお高い)。WAP150 は高速ローミングにも対応しているのですが、1F から 2F の移動時の再認証による速度低下は気にならないと思ったので使っていません。

SSID は以下のように 1F と 2F で揃えて設定します。ローミングに対応している端末なら自動で電波の強い方に繋ぎ直してくれるのでこれで十分です:

  • WAP150 1F
    • 2.4GHz: hoge
    • 5.0GHz: fuga
  • WAP150 2F
    • 2.4GHz: hoge
    • 5.0GHz: fuga

実測

この環境でスイッチ下の有線で接続している PC から速度を実測してみました。

930Mbps

おそらく Buffalo のスイッチで律速している気がしますね。時間をとって配線変えてチャレンジしてみたいと思います。(追記2023/03/14: IX2215 で律速のようです)

電気代について(追記: 2023/03/15)

IX2215 と WAP150 x2 の消費電力量はピークで 33W(= IX2215 14W + WAP150 9.5W x 2)、スマート分電盤による実測で 15Wh ほどでした。

テストマン、新築住宅の平面図からおこした3Dモデルを実写と比較する

こんにちは。この記事は下記記事の振り返りです。

blog.kuniwak.com

引き渡しの際に撮影した写真と、平面図から起こした3Dモデルをレンダリングして得たシミュレーション画像を比較することで3Dモデルの精度を確認しました。3Dモデルには当時私が平面図をどう認識していたかがよくあらわれていました。この認識と実際がどう異なっていたかを観察することで、平面図の誤解しやすい場所がわかりました。

なお良かったこと悪かったこと編は別に書く予定です。

TL;DR

図面から起こした 3D モデルは細かな差異はあるもののおおむね正確です。日照や照明のシミュレーションでは十分使える精度をもっていると確認できました。この細かな差異は次のとおりです:

  • 平面図には書かれていない部分(手すり・照明スイッチ・足元照明・コンセントの寸法と位置、床の高さなど)は推測でモデリングしているがその寸法・位置が実際とは異なっていた
  • 平面図・カタログの見落としがちらほらあった(火災報知器など)
  • シミュレーション画像では青空からくる環境光が再現されていなかった

前者2つを防ぐためには、床の高さのわかる図面、スイッチ・各種モニタ・足元照明の詳細な図面を要望するとより正確な 3D モデルを作れるでしょう。またカタログスペックとカタログの写真との比較も有効です。最後はどう修正すれば良いかわからなかったので、対応方法をご存知であればご教授願います。

実写と3Dモデルのシミュレーション画像を比較する

リビング入り口

リビングから廊下の眺め(前:実写、後:3Dモデルのシミュレーション画像)

おおむねよい精度でモデリングできていたようです。ただしよく見ると次の差異があります:

  1. シミュレーション画像では右奥の洗面台入り口がドアの上枠と揃っているが実際は揃っていない(原因:平面図では正確な指示がないため推測でモデリングしたところ実際とは異なる形状になった)
  2. シミュレーション画像に天井に階段下面の出っ張りはないが実際には出っ張りがある(原因:平面図では正確な指示がないため推測でモデリングしたところ実際とは異なる形状になった)
  3. ドアホンモニタの位置が異なる(原因:平面図では正確な位置の指示がないため推測でモデリングしたところ実際の位置からずれた)
  4. 実写にはある火災報知器が3Dモデルにはついていない(原因:図面には記載があったが見落とした)
  5. 奥の小窓の高さがずれている(原因:床の高さの推測値が実際の値よりも大きかった)
  6. 洗面台裏のハンガーポールの奥行きが違う(原因:引き渡し前に職人の方から図面よりもよい配置があると提案されこれを採用したため図面とは異なる施工になった。ありがたし)
  7. 実写は上の方が暗いが3Dモデルはそうなっていない(原因:床材のマテリアルの粗さが実際よりも高かった。言い換えると床からの光の反射が推測よりも指向性を持っていた)

平面図に記載がなく推測で補った部分に差異が出ていることがわかります。

キッチン

キッチン(前:実写、後:3Dモデルのシミュレーション画像)

背面のカウンターの高さを勘違いして5cmほど高くモデリングしていたようです。VR で試した時はキッチンの調理シミュレーションをしなかったため気づきませんでした。また引き出しの取手がつかないと誤解していため3Dモデルには取手がついていません。カタログに掲載されている写真では取手があったので私の見落としが原因です。

ダウンライトの位置が違う気がしますが、3Dモデルのレンダリングに使ったカメラの位置と向きが正確でないことが原因かもしれません。

換気扇は正確な図面をもらっていないので写真からの推測でモデリングしているため差異が出ています。

階段

階段(前:実写、後:3Dモデルのシミュレーション画像)

ここは前回の記事で3Dモデルでの照明シミュレーションにより曲がり角が暗い問題を発見した場所です。実写と見比べたシミュレーション画像の照らされている範囲は忠実に見えます。つまり修正前の図面のシミュレーションで曲がり角が暗かった問題も実際に暗かった可能性が高いです。照明のシミュレーションって大事ですね。

差異もあります。シミュレーション画像は実写よりも足元照明の色温度が高めになっています。照明の色温度は照明器具メーカーのカタログを見ればわかりますから、私の確認が十分ではありませんでした。

2F廊下

2F廊下(前:実写、後:3Dモデルのシミュレーション画像)

2F階段入り口はベビーゲートを設置する予定で図面では幅を確認していました。実際には手すりが干渉してつける予定だったベビーゲートをつけられませんでした。3Dモデルでも手すりの位置は実際より高くかつ奥についており、この事態を想定していなかったことがわかります。何度もベビーゲートつけたいという話はしていたのですが、手すりによりつけられなくなる事態は私、ハウスメーカーのいずれも気づいていませんでした。運よく取り付けられるベビーゲートを見つけられたのでことなきを得ましたが、取り付けられるベビーゲートは一種類しかなくオートクローズ機能のあるものを選べなくなったので事前に気づきたかったです。以下は取り付けられたベビーゲートですがオートクローズ機能はついていません。

item.rakuten.co.jp

実写では青空からの光で室内が青く照らされていますが、シミュレーション画像はほとんど青くありません。背景テクスチャは青空のものを使っていますがうまく再現できないようです。背景テクスチャからの放射のstrengthを調整しましたが、明るくなりはするものの青空の色はうまく出ませんでした。何かうまい方法をご存知の方がいたらぜひご教授願いたいです。

外構

外構は実写を載せたくないため画像は割愛します。

これまでの実写とシミュレーション画像との比較で最も差異が大きかったのが外構です。玄関ドアの材質の光の反射率やブロック塀の高さ、隣家との間のフェンスの高さなど図面には載っていないものが差異を生みました。いずれもハウスメーカーに要望しても返答の望みの薄い数値ですので、自分で測るほかありません。自分で測れない場合は差異が出るリスクを受け入れられるよう考慮しておくとよいでしょう。

脱線:シミュレーション画像と実写画像が重なるようにすると、実際のカメラとレンズの仕様と Blender のカメラ設定がなぜか一致しない

今回使ったカメラは SIGMA fp と 35mm F1.4 DG DN | Art です。

amzn.to amzn.to

Blender の設定

仕様によると SIGMA fp のセンサーの大きさは 36mmx24mm で 35mm F1.4 DG DN | Art の焦点距離は 35mm ですから、Blender のカメラもそのように設定すれば実写画像とシミュレーション画像が揃うはずです。実際にはセンサーサイズを揃えると焦点距離が 55mm ほどでないと画像が重なりませんでした。この差異の原因について心当たりがある方はご教授願います。

まとめ

図面から起こした 3D モデルはおおむね正確です。日照や照明のシミュレーションでは十分使える精度をもっていると確認できました。細かな差異を防ぐためには、床の高さのわかる図面、スイッチ・各種モニタ・足元照明の詳細な図面を要望するとよいでしょう。またカタログスペックとカタログの写真との比較も有効です。

ソフトウェアエンジニアでテストマンな私が家を買う際にやったこと

はじめに

ソフトウェアエンジニアでテストマンを生業とする Kuniwak です。今回は家を買うためにやったことを紹介します。 というのも、家を買うためにやったことを知人に話してみたら面白がられたため、誰かの役に立つかもしれないと思ったからです。

なおこの記事はソフトウェアに関する技術の記事ではありません(随所に検証の基本的な考え方などが散りばめられていますが…)。また、この記事で紹介する意見・手法は多分に cocopon 氏の影響を受けています。cocopon 氏の家購入エントリもこの記事と同時に公開されているはずです。

また、この記事はとても長いので先にポイントを説明しておきます。この記事ではライフプランシミュレーションに始まり次のような3Dモデルを作って日照や照明の検証をしていきます。また、3Dモデルを作るだけでは漏れが出るのでさまざまな検証を組み合わせています:

検証のために作った3Dモデルのレンダリング結果

目次

  • はじめに
  • 目次
  • 家を買うきっかけ(ライフプラン編)
    • ライフプランを考えはじめたきっかけ
    • ライフプランをシミュレーションするサイクルが長すぎる
    • ライフプランシミュレータを自作しよう
    • 自作ライフプランシミュレータのリスク
    • 自作ライフプランシミュレータから見えてきたこと
  • 家購入編
    • 中古・建売・注文どれにするか
      • 立地を比較するために評価方法を考える
        • 立地評価の項目を洗い出す
      • 間取りを比較するために評価方法を考える
        • 今後のシミュレーションのために間取り図を模写する
        • 家具の配置シミュレーションをする
        • 生活の変化のシミュレーションをする
        • 収納のシミュレーションをする
        • 第三者にレビューしてもらう
        • レビューの代わりに家事のシミュレーションをする
    • 物件の比較の結論
    • 注文住宅を購入する
      • 予算を確認する
      • 土地を検証する
      • 間取りと窓を検証する
        • 間取りを自分でも考えてみる
          • 外壁はプロに作ってもらう
          • 305mm グリッドで間取り図を作成する
          • 閑話:間取りの自動生成
        • 要求リストによる検証と収納シミュレーションによる検証をする
        • 壁厚を考慮したより現実の建物に近い図を作成する
        • 家具配置シミュレーションと生活の変化のシミュレーションをする
        • 3D に起こして間取りの見え方を検証する
          • 下準備
            • 高さの目安の準備
            • Blender に jlampel/extra_lights を入れておく
          • 間取りの見え方を確認する
        • 3Dモデルで窓を検証する
          • 隣接する建物を再現する
          • Sun Position プラグインで日照をシミュレーションする
      • 詳細図面を検証する
        • コンセントを検証する
        • 照明を検証する
          • スイッチ位置を評価する
        • 窓の 3D モデルで評価できてないない部分を検証する
        • VRゴーグルで中を歩く
      • 外構を検証する
      • 見つかった問題を振り返る
  • おわりに
  • 後日談
続きを読む

Isabelle ゆるリファレンス(apply-scriptスタイル)

最近は本業の自動テストマンより定理証明支援系に力を入れている Kuniwak です(経緯は過去記事参照)。

orgachem.hatenablog.com

定理証明支援系はいいぞ、ということで本記事では 私がよく使う Isabelle の便利機能の紹介とごく個人的に感じている使い所などを書いています

そのため全機能を網羅するものではなくかなり漏れがあります。もし「これめっちゃ便利だけど書いてない!」みたいな気づきがありましたらご指摘いただけると嬉しいです!

本記事の対象読者

対象読者は Isabelle の apply-script スタイルを学びたい方で、かつ「Tutorial on Isabelle/HOL 2020 の 1, 2, 3, 5 章を読んでなんとなくわかったけど…」な方です。もし定理証明支援系を使ったことがなく、Tutorial もまだ読んでいない方は 1, 2, 3, 5章を先に読むことをお勧めします(これ以外の章は重要度が低いかあるいはこの記事を読むのに必要なレベルを超えます):

  • 1章: 基礎(The Basics)
  • 2章: HOL の関数型プログラミング(Functional Programming in HOL)
  • 3章: さらなる関数型プログラミング(More Functional Programming)
  • 5章: このゲームのルール(The Rules of the Game)

またかなり前の Tutorial の日本語訳であれば caeruiro さんによる邦訳があります

ちなみに実のところ私は Tutorial で Isabelle を学んだわけではなく、PRINCIPIA が開催していた Isabelle チュートリアル全4回で上記1-5章+6-7章の内容を学びました(有料なので遠慮なく質問しまくった)。

Isabelle ゆるリファレンス

続きを読む

自動テストに限界を感じた私がなぜ形式手法に魅了されたのか

長らく自動テストとテスト容易設計を生業としてきましたが、最近は色々な限界を感じて形式手法に取り組んでいます。

この記事では、既存の自動テストのどこに限界を感じてなぜ形式手法が必要なのか私見を説明します。なお、私もまだ完全理解には程遠いため間違いがあるかもしれません。ご指摘やご意見はぜひ Kuniwak までいただけると嬉しいです。

著者について

プログラマです。開発プロセスをよくするための自発的な自動テストを支援する仕事をしています(経歴)。ここ一年は R&D 的な位置付けで形式手法もやっています。

自動テストの限界

自動テストとは

私がここ数年悩んでいたことは、iOS や Web アプリなどのモデル層のバグを従来の自動テストで見つけられないことでした。ただ、いきなりこの話で始めると理解しづらいと思うので簡単な例から出発します。

この記事でいう自動テストとは以下のようにテスト対象を実際に実行して結果を確認する方法を意図しています:

// 自分で実装した add で 1+1 が 2 になるかを検証する自動テスト。
describe('add', () => {
  context('when given 1 and 1', () => {
    it('should return 2', () => {
      const actual = add(ONE, ONE);

      const expected = TWO;
      assert.deepStrictEqual(actual, expected);
    });
  });
});

この例では自分で実装した add 関数の挙動を確認するために add(ONE, ONE) の結果が TWO と等しくなることを検証しています。 ただ、足し算の動作確認として 1 + 1 だけを確認しても安心はできないですから、これに加えて別のテストケースを足していくことになります。

なお、先に今回の話の前半のポイントを提示しておくと、このように「テストケースの追加」を続けていくとどのように考え方を変えなければならないかということです。

テストケース増加への対応の限界

さて、1 + 1 = 2 以外のテストケースとして 1 + 0 = 1 を検証しようとすると、次のように同じような単純なテストケース(Single Condition Test)を増やすことになるでしょう:

describe('add', () => {
  context('when given 1 and 1', () => {
    it('should return 2', () => {
      const actual = add(ONE, ONE);

      const expected = TWO;
      assert.deepStrictEqual(actual, expected);
    });
  });

  // 別の入力を試したいならテストケースを分ける。
  context('when given 1 and 0', () => {
    it('should return 1', () => {
      const actual = add(ONE, ZERO);

      const expected = ONE;
      assert.deepStrictEqual(actual, expected);
    });
  });
});
ここまでの流れ
単条件のテストケース追加

これも悪くないですが、1つのテストケースを足すたびに8行ほどの記述が必要なのは少し辛いですね1。そこで、この例のように複数のテストケースが入力値だけ異なるとき次のような共通化を図ります(Parameterized TestやTable Driven Testと呼ばれる):

describe('add', () => {
  [
    {lhs: ONE, rhs: ONE, expected: TWO },
    {lhs: ONE, rhs: ZERO, expected: ONE },
  ]
  .forEach(({lhs, rhs, expected}) => {
    context(`(${lhs}, ${rhs})`, () => {
      it(`should return ${expected}`, () => {
        const actual = add(lhs, rhs);

        assert.deepStrictEqual(actual, expected);
      });
    });
  });
});

こうすると、新たなテストケースを足すのに必要な行数はたったの1行となり保守が楽になります。ここまでは自動テストでも余裕で対応できます。

ここまでの流れ
単条件のテストケース追加 → Parameterized Test

ところで、今回のテスト対象である足し算をきちんと実装できたと安心できるテストケースの数はどれぐらいでしょうか?少なくともいま試した2つだけでは安心できないはずです。このようなときにテストマンがよく使う手法は「同値分割」と「境界値分析」です。これらはいずれも試さないといけない入力が大量にある状況を現実的な時間で対処するために入力を間引くための方法です。同値分割は出力が同じになる入力から代表値を選ぶという方法で入力を間引きますし、境界値分析では内部の条件分岐のちょうど境目付近を通過する入力はなるべく間引かない方法とも言い換えられるでしょう。

今回は厳密な同値分割はせず、ざっくり小さい値と大きな値から代表値をテストケースに加えるとしましょう。ついでに境界値になりそうな 0 も入れておきます:

describe('add', () => {
  [
    {lhs: ONE, rhs: ONE, expected: TWO }, // 小さい値と小さい値の組み合わせ
    {lhs: ONE, rhs: ZERO, expected: ONE },
    {lhs: ONE_MILLION, rhs: ONE, expected: ONE_MILLION_ONE }, // 大きい値と小さい値の組み合わせ
    {lhs: ONE, rhs: ONE_MILLION, expected: ONE_MILLION_ONE }, // 小さい値と大きい値の組み合わせ
    {lhs: ZERO, rhs: ZERO, expected: ZERO }, // 境界値になりそうな値
  ]
  .forEach(({lhs, rhs, expected}) => {
    context(`(${lhs}, ${rhs})`, () => {
      it(`should return ${expected}`, () => {
        const actual = add(lhs, rhs);

        assert.deepStrictEqual(actual, expected);
      });
    });
  });
});

このように足せました!さらに慎重な方ならば、自然数型の最大値も境界値になりそうなのでこれも加えるでしょう(この例は簡単にするために最大値を設けていない)。

ここまでの流れ
単条件のテストケース追加 → Parameterized Test → 同値分割・境界値分析

さて、ここまで検査対象の入力を広げてきましたが、これなら安心できるでしょうか?以前の私であればこのあたりを現実的な安心のラインとして手を止めていました。実はこの判断は「大抵のバグは入力空間の小さい部分を探すだけで見つかる」という小スコープ仮説2を暗黙に仮定しています。この例ではよっぽど妙な実装をしない限りは成り立つと感じますが、検査する対象によっては成り立たないこともあります(特に後述する並列システムなど)。ここでは説明を簡単に保つために、この仮説が足し算でも成り立たないとして話を進めます。

ということで、安心できるようになるまでテストケース数を増やしていきましょう。ただ、そろそろ手で追加してくるのが厳しくなってきたはずです。このような時にテストマンが使う方法は QuickCheck に代表される Property-based Testing です。Property-based Testing ではこれまでの手法とは異なり入力をプログラマが指示しません。代わりに型や値の生成器が入力を自動的に大量に生成してくれます。

なんて便利なんだ!と思うかもしれませんが、実はここで考え方の転換が必要になります。今までのコードを思い出して欲しいのですが、自動テストでは期待する出力と実際の出力の比較という形で記述してきました:

// これまでのコード
describe('add', () => {
  context('when given 1 and 1', () => {
    it('should return 2', () => {
      const actual = add(ONE, ONE); // プログラマが入力として 1, 1 を選び、

      const expected = TWO; // 期待する出力として 2 を指定する。
      assert.deepStrictEqual(actual, expected);
    });
  });
});

しかし、入力が自動生成されるのであれば期待する出力をどのように指定すればいいのでしょうか?

// Property-based Testing のコード
describe('add', () => {
  context('when given X and Y', () => {
    it('should return a result same as result of Y and X', () => {
      const [x, y] = valuesGen(/* 値の生成器が自動的に何らかの自然数を選ぶので、*/);
      const actual = add(x, y);

      const expected = '???'; // 期待する出力として ??? を指定する。
      assert.deepStrictEqual(actual, expected);
    });
  });
});

何となく考え方を転換しないといけない部分がわかってきたでしょうか。Property-based Testing では入力に対する具体的な期待出力を指定できないのです。代わりに 入力と出力の間で成り立つ関係を利用します。例えば足し算は順序を変えても結果が同じはず(x + y = y + x)ですから、次のように引数の順序をひっくり返しても結果が同じになるといった指定をします:

describe('add', () => {
  context('when given X and Y', () => {
    it('should return a result same as result of Y and X', () => {
      const [x, y] = valuesGen(/* 値の生成器が自動的に何らかの自然数を選ぶので、*/);
      const actual = add(x, y);

      const expected = add(y, x); // 順序を反対にしても計算結果が変わらないことを確かめる。
      assert.deepStrictEqual(actual, expected);
    });
    // 実際には valuesGen の値を変えて大量に繰り返す。
  });
});

これだけだと全く不十分ですから、次のように足し算の持つ様々な入出力の間の関係の検証も追加していきます:

describe('add', () => {
  context('when given X and Y', () => {
    it('should return a result same as result of Y and X', () => {
      const [x, y] = valuesGen(/* 値の生成器が自動的に何らかの自然数を選ぶので、*/);
      const actual = add(x, y);

      const expected = add(y, x); // 引数の順序を反対にしても計算結果が変わらないこと(交換法則)を確かめる。
      assert.deepStrictEqual(actual, expected);
    });
  });

  context('when given (X and Y) and Z', () => {
    it('should return a result same as result of X and (Y and Z)', () => {
      const [x, y, z] = valuesGen(/* 値の生成器が自動的に何らかの自然数を選ぶので、*/);
      const actual = add(add(x, y), z);

      const expected = add(x, add(y, z)); // 計算する順序を変えても結果が変わらないこと(結合法則)を確かめる。
      assert.deepStrictEqual(actual, expected);
    });
  });

  context('when given X and 0', () => {
    it('should return X', () => {
      const [x] = valuesGen(/* 値の生成器が自動的に何らかの自然数を選ぶので、*/);
      const actual = add(x, ZERO);

      const expected = x; // 0 と足しても元の数字と同じになること(単位元の存在)を確かめる。
      assert.deepStrictEqual(actual, expected);
    });
  });

  context('when given X', () => {
    it('should return something not equal X itself', () => {
      const [x] = valuesGen(/* 値の生成器が自動的に何らかの自然数を選ぶので、*/);
      const actual = add(x, ONE);

      const unexpected = x; // 1を足せば結果はXとは異なることを確かめる。
      assert.deepStrictEqual(actual, expected);
    });
  });

  // 実際には valuesGen の値を変えて大量に繰り返す。

  // 前の Parameterized Test も残しておくとより安心。
});

このようにして入力を自動生成すれば手で書いた時よりもずっと多くできるわけですから、より強い安心を得られるわけです。

ただし代わりに失われたものもあります。Parameterized Testまでは具体的な期待を書けたため深く考えなくてもよかったのですが、Property-based Testing では具体的な期待を書けなくなったためにより抽象度の高い「入力と出力の間の関係」を見抜かねばなりません。これには一定の訓練が必要です。

ここまでの流れ
単条件のテストケース追加 → Parameterized Test → 同値分割・境界値分析 → (入出力の具体性の壁) → Property-based Testing

さて、実はこれでもまだテストケースは完全ではありません。自動生成された値は手で作られたよりもずっと多いですが、それでも入力可能な値(自然数型の値すべて)よりはずっと少ないのです。ただ、自然数の例では前述の小スコープ仮説(大抵のバグは入力空間の小さい部分を探すだけで見つかる)が成り立つでしょうから、きっと入力すべての動作確認は不必要に思えるでしょう。しかし後述する並行システムでは小スコープ仮説が成り立たないことが多く、したがって網羅性は依然として重要なのです。そのためもう少し網羅性の話を続けます。

なお、これまでの手法の延長線上で入力を網羅的に扱う方法として、網羅的な入力が可能なレベルまで入力可能な値の数を減らす方法があります。自然数の例でいうと利用したい最大の自然数がそこまで大きくないのであれば、最大値を設けてそこまでの入力すべてを網羅的に検証する方法をとれます(この考え方は後述する並行システムでも重要)。ここまできて、ようやく従来の方法の延長線上でも網羅的な動作確認が可能になりました。

ここまでの流れ
単条件のテストケース追加 → Parameterized Test → 同値分割・境界値分析 → (入出力の具体性の壁) → Property-based Testing → (網羅性の壁) → 可能なら入力空間の制限

ただしこの方法は入力を絞れるときのみ使える方法だったことを思い出してください。仕様によってはこれが適用できないことも多く、特に並行システムにおいてはこの方法をとってもなお入力空間が広すぎる場合が頻繁におきえます。

そして、ここから先こそが形式手法の力を借りないといけない世界なのです。

ここまでの流れ
単条件のテストケース追加 → Parameterized Test → 同値分割・境界値分析 → (入出力の具体性の壁) → Property-based Testing → (網羅性の壁) → 可能なら入力空間の制限 → (扱える入力空間の大きさの壁) → 形式手法(???)

網羅性への挑戦

ここから先は、形式手法の1つである定理証明支援系を使って広い入力空間を検証してみましょう。今回は Isabelle という定理証明支援系のシステムを駆使して検査したい対象の入力すべてに対して性質を確認します。ただ、このように説明されてもなかなか飲み込めないと思いますので、もう少し自然数の例を続けていきます。

さて、今回の add の実装が次のようだったとします:

// add 関数の入力値は Zero または Succ のいずれか。Zero は 0 を意味し、Succ はパラメータの自然数を +1 した自然数を表現する。
// つまり、new Succ(new Zero()) は 0 + 1 なので 1 を意味し、new Succ(new Succ(new Zero())) は (0 + 1) + 1 なので 2 を意味する。
class Zero {}
class Succ {
    constructor(nat) { this.v = nat }
}


function add(a, b) {
  // もし b が 0 なら a が結果(a + 0 = a ということ)。
  if (b instanceof Zero) {
    return a;
  }

  // もし b が 0 でなければ b の Succ 前の値(b - 1)と a に対して再帰的に add を呼び出して、
  // 最後に全体として +1 する(つまり (a + (b - 1)) + 1 = a + b ということ)。
  // こうすると、最終的に b が 0 になるまで再帰的に add が繰り返され、b が 0 になった時点の
  // 結果で再帰が停止して b の自然数の回数分 +1 して値が戻ってくる。
  return new Succ(add(a, b.v));
}

そして上の JavaScript のコードは次の Isabelle での表現と対応するとしましょう(=JavaScript のコードと Isabelle のコードがまったく同じ振る舞いをするとしています3):

(* JavaScript の Zero と Succ のクラスに対応する型を nat として定義する。 *)
datatype nat = Zero | Succ nat

(* add は JavaScript の add と同じ再帰的な定義とする。 *)
primrec add :: "nat =>> nat => nat" where
  "add a Zero = a"
| "add a (Succ b) = Succ (add a b)"

Isabelle ではこのように記述された定義に対して「証明」が可能です。Isabelle で記述されたコードは、数学の証明のように論理的な推論によって結果がどうなるかを推測できるためです。なお、この方法でも Property-based Testing と同様に入出力の間で成り立つ関係を使います。

例えば、前の Property-based Testing で検証した x + 0 = x は次のように Isabelle で記述でき、しかも今回は全自動で証明できます(Isabelle は自動的な証明に優れている):

(* すべての自然数 x について x + 0 = x が成り立つことを証明する。 *)
theorem "∀x. add x Zero = x"
  by auto (* 自動での証明を指示(自動で解ける)。 *)

この証明が成功するということは、どんな自然数 x であっても x + 0 = x を満たす と確認できたことを意味します。ここで重要なのは「どんな自然数でも」という部分で、今回は自然数の定義の上限を与えていませんからとてつもなく大きな自然数であってもこれが成り立つのです4

しかしおそらくこれでは実感が湧かないと思うので、試しに add の定義をわざと間違えてどうなるかを見てみましょう:

primrec add :: "nat ⇒ nat ⇒ nat" where
  "add a Zero = Zero" (* わざと間違えてみる *)
| "add a (Succ b) = Succ (add a b)"

この状態で、先ほどの x + 0 = x を証明しようとすると証明は失敗します:

(* すべての自然数 x について x + 0 = x が成り立つことを証明する。 *)
theorem "∀x. add x Zero = x"
  by auto (* 自動での証明を指示(失敗する)。 *)

(* Isabelle の出力:
  theorem add_iden: ?x = add ?x Zero
  Failed to finish proof⌂:
  goal (1 subgoal):
   1. x = Zero
*)

よくみると subgoal: x = 0 という出力になっています。これはどんな自然数 x に対しても x = 0 が成立するなら上記の間違った定義でも証明できると Isabelle が教えてくれています。要するに x が 0, 1, 2, 3, ... のいずれでも x = 0 が成立すれば証明できるということですが、x が 0 以外については x = 0 が成立するとまずいわけですから、どこかで間違えたとわかるのです。このように Isabelle は証明したいことがらを論理的な推論によって証明できる極めて強力な手段となります。

また、次のように x + 0 = x の例以外の性質もほぼ自動で証明できます(一部は人間の手で補題を切り出してヒントを与えている):

(* 自動推論のための手がかりを与えるための補題。 *)
lemma add_iden [simp]: "x = add x Zero"
  by auto


lemma add_assoc: "add (add x y) z = add x (add y z)"
  apply(induct_tac z)
  by auto


lemma [simp]: "Succ (add x y) = add (Succ x) y"
  apply(induct_tac y)
  by auto


lemma add_comm: "add x y = add y x"
  apply(induct_tac y)
  apply(auto)
  apply(induct_tac x)
  by auto


(* 引数の順序を反対にしても計算結果が変わらないこと(交換法則)を確かめる。*)
theorem "∀ x y. add x y = add y x"
  apply(intro allI)
  by (rule add_comm)


(* 計算する順序を変えても結果が変わらないこと(結合法則)を確かめる。*)
theorem "∀ x y z. add (add x y) z = add x (add y z)"
  apply(intro allI)
  by (rule add_assoc)

まとめると、ここでは定理証明支援系を使えば入力空間を制限できない状況であっても網羅的な検査ができるとわかりました。なお、これ以外の例でよりパターン数が多い状況でも Isabelle は高い威力を発揮できると知られています(ケプラー予想の形式証明に Isabelle が使われた例が参考になる)。

ここまでの流れ
単条件のテストケース追加 → Parameterized Test → 同値分割・境界値分析 → (入出力の具体性の壁) → Property-based Testing → (網羅性の壁) → 可能なら入力空間の制限 → (扱える入力空間の大きさの壁) → 定理証明支援系による形式検証

現実的な例

ここまでの例では、説明を簡単にするため極めて単純化された非現実的な例を扱ってきました。そこでここからは現実的に形式手法が必要になる状況を説明していきます。

私がこれまで悩んできたのは、iOS アプリや Web アプリにおいて多用する協調して動作する状態機械のバグを従来の自動テストで見つけられないことでした。iOS アプリに限らず、何らか状態をもつオブジェクトは状態機械として設計しておくと状態の大局観の把握が楽になります。そのため、状態機械は頻出の設計パターンとなります(具体的にどのようなコードになるかは Kuniwak/reversi-ios を参照)。なお、これらの状態機械には状態が変わったことを監視できるイベントストリームを用意しておいて、View はここから変更を検知して見た目を変更するように構成します。

さて、これが単純な状態機械ただ1つなら話は簡単でした。問題は現実的なアプリは小さな状態機械1つでは表現できないため、小さな状態機械を複数組み合わせる必要があるという点です(この方法は一般的に Hierarchal Finite State Machine として知られている)。

一般に、このような 並列動作する状態機械の組み合わせの検証に必要なテストケース数は人間の想像を超えてきます 。例えば、次の図はとあるイベントで実装したアプリの状態機械全体の状態遷移図です(全部載り切らないのでごく一部):

f:id:devorgachem:20200524132128p:plain全体

この図を一見しただけで、Property-based Testing を含む従来のいずれの方法もほぼ無力だとわかると思います。なお、この中のごくわずかな部分にバグが潜んでいて、実際に今回のリバーシアプリでもアニメーションに一貫性のないバグが後から見つかりました。この原因はイベントストリームの購読者が2ついることに気づいておらずしかも購読者の呼び出し順序が期待と違っていたことが原因でした(この図の中のいずれかの遷移に想定外の遷移がたった1つあったのが原因)。

この話を一般化すると、よくマルチスレッドプログラミングの開発が難しいと言われるのは、この膨大な数の組み合わせのなかのごくわずかな部分にデッドロックや無限ループなどの欠陥が潜んでいることに人間が気づけないからです。そして、これまで見てきたように、膨大の入力の組み合わせ数があるとき自動テストを含む動的検査は無力です。だから形式手法が重要になるのです。

なお上記のアプリについては、今回紹介した定理証明支援系とは別の形式手法である「モデル検査5」を試みました(詳細)。モデル検査は並列システムの網羅的な検証を得意としていて、私の悩みを解決できる有力な手段の1つです。このモデル検査も巨大な状態遷移グラフを扱う際には具体的な入出力を指示できないため、これまで同様に抽象的にものごとを捉えなければならない点が共通しています。ちなみにモデル検査は次ぐらいの位置付けです:

ここまでの流れ
単条件のテストケース追加 → Parameterized Test → 同値分割・境界値分析 → (入出力の具体性の壁) → Property-based Testing → (網羅性の壁) → 可能なら入力空間の制限 → モデル検査 → (扱える入力空間の大きさの壁) → 定理証明支援系による形式検証

テストケースの増加につれ起こったこと

これまでの流れを少し振り返りましょう。自然数の足し算を例にテストケースの数が増えるにつれ次の箇条書きリストの下の方へ手法が移り変わってたことを見てきました:

  1. 単条件のテストケース追加
  2. Parameterized Test
  3. 同値分割・境界値分析
  4. ──(入出力の具体性の壁)──
  5. Property-based Testing
  6. ──(網羅性の壁)──
  7. 可能なら入力空間の制限
  8. モデル検査
  9. ──(扱える入力空間の大きさの壁)──
  10. 定理証明支援系による形式検証

要するに、下の方の手段である形式手法(8と10)は膨大な量のテストケースを現実的な記述量で表現するための方法です。そのため抽象度が上がってしまい捉えづらくなってしまうということなのでしょう。

このような事情も相まって、形式手法を上手に扱うのにはかなりの知識が必要です。実際に取り組んでみるとわかるのですが、まず人間は意図通りの論理式やモデルを書けません(必要な条件を忘れていたりそもそも意図が間違ってたりする)。当たり前の話ですが、従来のコードでさえ正しく書けないのに、より抽象度の高い論理式やモデルを最初から正しく書けるはずがないのです

したがって形式手法においても自分の記述したモデルや論理式が自分の意図通りになっているか確かめる作業は避けて通れません。だからこそ、Isabelle や他の形式手法系のツールが何を言っているのかを理解するための数理論理学の学習や基礎理論への理解がとても重要になります。さらに、このような知識とは別に TDD のように論理式やモデルをこまめに確認できるよう準備しておくといった進め方のノウハウも重要になります(ノウハウの例)。

私もまだまだ知識は足らないですが、以前とは異なり今は次のステップが見えるようになりました。あとは一歩ずつ進んでくだけだと思っていて、今も形式手法を学びつつ実践に取り組んでいます。

まとめ

この記事では、以下の3点を説明しました:

  1. テストケースの増加につれ従来の自動テストに限界が訪れる
  2. 現実の設計で頻出する状態機械の合成はこの限界を超えてくる
  3. これに対応できる手段が形式手法である

この記事をきっかけに、私のように形式手法に興味をもつテストマンが増えるといいなあぁと思っています。


  1. 実際は1つのテストケースで複数の入力を連続して試すコードも多く見受けられますが、これは Eager Testと呼ばれるアンチパターンです。これには2つの問題があります。1つめの問題はテストケースの最初の方で assertion が失敗すると後続の検証が実行されず、テスト失敗の情報量が少なくなるというものです。2つめは、複数の入力を1つのテストケースでまとめてしまうとテストケースの名前が曖昧になります(テストの意図を推測する上で重要な情報が失われる)。これらの問題を避けるため、この記事のように1つの試したい入力に対して1つのテストーケースが対応するようにしましょう。

  2. Andoni, Alexandr; Daniliuc, Dumitru; Khurshid, Sarfraz; Marinov, Darko (2002). “Evaluating the small scope hypothesis”

  3. ここで厳密な対応への言及を避けたのは JavaScript の形式化された意味論が必要だからです。意味論が形式的に定められた処理系(例えば KJS)以外では、Isabelle での表現と本当に対応しているのかどうかを検証(正当性証明)するのはとても大変です。

  4. 元の JavaScript のコードはほとんどの処理系で 1000 程度の自然数で stack overflow でエラーになります(本当は末尾再帰最適化可能な形にしたかったんですが証明で手間取ったので諦めた…)。重要なのは JavaScript のコードと Isabelle のコードの対応関係で、このように再帰が深くなる場合に対応関係が崩れるため証明の結果と実際の JavaScript での実行結果が一致しません。もしこれを含めた検証をしたければ、このような JavaScript の振る舞いを形式的に定義した意味論に踏み込む必要がありますが、今回は説明を簡単にするためにこれを避けています。

  5. モデル検査の定義を説明するのはとても大変なので、定義を知りたい方は「モデル検査入門」などを読むといいかもしれません。