かまたま日記3

プログラミングメイン、たまに日常

Goでtesting.T#Helperを使う

Goのテストでヘルパーメソッドを作って共通のアサーションを定義することがあるかと思います。

package test

import "testing"

func Add(x, y int) int {
    return x + y
}

func TestAdd(t *testing.T) {
    AssertEquals(t, 2, Add(1, 1))
    AssertEquals(t, 3, Add(1, 2))
    AssertEquals(t, 4, Add(1, 3))
}

func AssertEquals(t *testing.T, expected, actual int) {
    if expected != actual {
        t.Errorf("Unexpected int\nexpected:%d, actual:%d", expected, actual)
    }
}

ここで言う AssertEquals が共通のアサーションですね。 これをこんな感じで、わざと失敗させてると以下のような出力になります。

func TestAdd(t *testing.T) {
    AssertEquals(t, 1, Add(1, 1))
    AssertEquals(t, 2, Add(1, 2))
    AssertEquals(t, 3, Add(1, 3))
}
=== RUN   TestAdd
    TestAdd: add_test.go:17: Unexpected int
        expected:1, actual:2
    TestAdd: add_test.go:17: Unexpected int
        expected:2, actual:3
    TestAdd: add_test.go:17: Unexpected int
        expected:3, actual:4
--- FAIL: TestAdd (0.00s)
FAIL

出力される行番号が全部ヘルパーメソッドの位置になってて実際のテストのどこで失敗したかが分かりづらいですね。 *1

こういうときは t.Helper() を呼ぶとそのヘルパーメソッドがcallerのスタックから除外されます。

AssertEquals関数を以下のように変更して実行します

func AssertEquals(t *testing.T, expected, actual int) {
    t.Helper()
    if expected != actual {
        t.Errorf("Unexpected int\nexpected:%d, actual:%d", expected, actual)
    }
}
=== RUN   TestAdd
    TestAdd: add_test.go:10: Unexpected int
        expected:1, actual:2
    TestAdd: add_test.go:11: Unexpected int
        expected:2, actual:3
    TestAdd: add_test.go:12: Unexpected int
        expected:3, actual:4
--- FAIL: TestAdd (0.00s)
FAIL

実際のアサーションを実行しているテストケースの行番号になりました。

まとめ

テストのヘルパーメソッドでは t.Helper() を呼び出すべし

*1:本来はこういうテストは一つのケースにまとめないでケースを分けるなどをした方が良いと思います

SEQSENSEの会社・仕事の紹介

はじめに

このエントリーは SEQSENSE株式会社 への採用 (主にクラウドチームのエンジニア) を目的としたエントリーです。もし興味を持った方は Findy or Wantedly から、もしくは @kamatama_41 まで直接連絡ください。

SEQSENSEについて

2016年10月に創業したロボットのスタートアップです。惑星探査機はやぶさに搭載されたローバーのミネルバの開発にも携わった明治大学の黒田洋司教授が中村CEOと組んで創業しました。

そういった経緯もあり、コアとなる技術は高度な自律移動性能を持ったロボットの製作技術なります。現在は深刻な人手不足に悩む警備業務にフォーカスを当てて、ロボット警備のサービスを提供しています。

2019年8月に商用化を開始して、2020年8月時点で都心のオフィスビルを中心に6つの施設で警備ロボットが稼働しています。 今年の2月には成田空港でもSEQSENSEのロボット (SQ-2) の導入が開始しました。

jidounten-lab.com

  • 『世界を変えない』
  • 『ヒト型も、ネコ型も、目指さない。』

というちょっと変わったミッション・開発理念を持っていますが、これは「本当に実用性のあるロボットを世に送りだして、人口減少に向かう社会を、テクノロジーの力で悪い方向に変えない様にしよう」という意味を持っています。詳しくは以下のインタビューが参考になるかと思います。

xtech.mec.co.jp

最近YouTubeチャンネルができました

www.youtube.com

チームについて

人数はSlackの#randomにいる人が38人で、数えてみるとエンジニアとそれ以外の人が2:1くらいの割合でした (2020年8月時点)

エンジニアリングチームは大きく4つに分かれていて

  • ロボハード (ロボットのハードウェアの設計・製造)
  • ロボソフト (ロボ上で動く自律移動ソフトウェアの開発)
  • クラウド (ロボットを管理するWebアプリなどの開発)
  • AI (画像認識モジュールの開発)

というチームで仕事を行っています。全然使っている技術が違うので、普段はそれぞれのチームで開発を行っていますが、当然チームをまたいだ機能開発なんかもあるので、その場合は連携して仕事を行います。

クラウドチームの仕事

ここからは私の所属しているクラウドチームの仕事の紹介になります。

クラウドチームが担当しているのは主にクラウド上で動くアプリケーションの開発・運用です。具体的には以下のようなモジュールがあります *1

  • 顧客 (警備員) が利用するWebアプリケーション
  • 管理者 (我々) が利用するWebアプリケーション
  • ロボットから上がってくる情報を処理するWorker
  • ロボット上で動くクラウドと情報のやり取りを行うagentプログラム
  • ロボットからの動画・音声を配信するサーバ

技術スタックとしては、顧客用のWebアプリケーションがRuby (on Rails)で書かれている以外は、バックエンドの処理はほぼGo言語で書かれています。フロントエンドはReact (Next.js)、内部サービス間の通信ではgRPC, GraphQLなんかが使われています。あとロボットから送られてくる動画をWebで配信する際にWebRTC (pion) を使っていたりもします。クラウドAWSを使っていて、前述のアプリケーションすべてがECS上のDockerコンテナとして動いています。

その他監視はDatadog, CIはTravisGitHub Actions, インフラ管理はTerraformを使っています。

仕事の進め方としては、すべてのチケットはGitHubのissueとして上げるようにしていて、2週間に1回のスプリント計画MTGで各人がissue一覧から何をやるかを決めて進めていきます。細かい見積もりとかスプリントバックログを作ったりといった厳密さはないゆるゆるなスクラム*2でやっています。今の所は人数が多くないのとそれぞれのメンバーの自律性が高いので大きな問題は出ずにやれています。

システム的な詳細は6月にAWS IoT Loftで発表したときの資料が詳しいです

kamatama41.hatenablog.com

クラウドチームの技術的課題

割と大きめなのを上げるとこんなのがあります。当然この他にもサービスの機能追加・改修などは随時発生しています。

管理アプリケーションのリプレース

社内用の管理アプリケーションは歴史的経緯から、現在Django + jQueryで構築されているのですが、メンテナンス性に課題があるため *3 現在React (Next.js) + GraphQLの構成の新アプリケーションに移行中です。旧アプリケーションの機能がたくさんあるため、完全移行までにはもう少し人手が必要です。

OTA (over-the-air) アップデートの仕組み構築

ロボットのソフトウェアをクラウド経由で自動でアップデートする仕組みを構築する。

  • Docker imageの更新
  • systemdサービスの更新
  • その他細かいプロビジョニング
  • など

現在はメンテナンス時間を取って手動で作業を行ってるのですが、稼働するロボットが100台を超えるスケールになると回らなくなるので、この仕組の構築は急務で、現在はAWS IoT Jobsとスケジューラ (今の所Rundeckを検討中) で行う予定です。

データ分析の基盤構築

機能開発などで後回しにしていたのですが、社内課題的にもデータ分析をしないとそろそろ厳しいなという時期になって来たので取り組み始めたところです。サービス上にある各種統計情報・ユーザーのアクセス情報などデータを集めるところからBIダッシュボードツールを入れてレポーティングするところまで、やること盛りだくさんです。

エレベーター連携

現在は三菱電機さんとクラウド側で連携してロボットをエレベーターに乗せているのですが

(参考) www.risktaisaku.com

他のベンダーさんともエレベーター連携の話があり、そのための連携用のサーバを構築する必要があります。やることとしては主に、ベンダーさん側の仕様を読んで、連携処理を実装し、実際の動作テストを行う、などです。

SEQSENSEのクラウドチームで働いて面白いところ

いわゆる普通のWebサービスと一番違うところは、実際のモノの開発に携われるところになります。IoT的なロボットとの双方向のデータのやり取りだったり、動画配信、エレベーター連携などは同じ規模のWeb系スタートアップではなかなか経験できないところではないでしょうか。また、ハードウェアの設計からやってるので多種多様なバックグラウンドの人と関われるところだったり、まだ未開拓の ロボット警備 という市場を1から作っていけるというのも、大変ですが面白いところだと思います。

*1:この他にも色々あります

*2:といって良いかも微妙なレベル

*3:DjangojQueryが悪いと言うよりは社内の技術スタック的な問題で

AWS IoT@Loft #11 で登壇してきた

iot-loft.connpass.com

スマートビルディングにおけるIoT活用の取り組み
というお題でスマートビルディングを警備するSEQSENSEのロボットのサービスについてお話してきました。

初めてのオンライン登壇、自分が話するときは200人近くの接続者がいたようで結構緊張しましたが、無難にこなせて(?) 良かったです。

スライドを見て興味を持っていただいたら是非ともお話しましょう!

www.wantedly.com

(追記) AWSさんの記事も投稿されました aws.amazon.com

RenovateでGoのプロジェクトの依存性を更新する

弊社でRenovateを使ってるので、知見のメモです

Privateレポジトリを含む場合

Renovateを適用したいGoのレポジトリのgo.modに同じorganizationの別のprivate repositoryを含む場合*1、その参照されているレポジトリにもRenovateを適用すれば、Renovateが持つGitHubトークンでそのprivate repositoryもgo mod download出来ます。

Support private go modules on github.com · Issue #3202 · renovatebot/renovate · GitHub

you don't need to check in a github token - Renovate will reuse the existing token for github.com. If you are running via the app, that means it uses the same token as Renovate uses to access the current repository, so you need to make sure Renovate is installed into the source repo too (Renovate can be disabled, but it must be installed in order to have permissions to read the contents).

一部のライブラリ更新頻度高すぎ問題

弊社の場合、主に golang.org/x/aws-sdk-goなんですが、頻繁に更新が来すぎて鬱陶しいです。他にも適宜更新にしていると結構業務時間取られるので、弊社では以下のようなポリシーにしています

  • 外部パッケージは週末にアップデート (して週明けにまとめてマージ)
  • 内部モジュールは更新され次第

renovate.json的にはこんな感じです (抜粋)

{
    "extends": [
        "config:base",
        "schedule:weekends",
    ],
    "postUpdateOptions": [
        "gomodTidy"
    ],
    "packageRules": [
        {
            "packagePatterns": [
                "^github.com/seqsense/"
            ],
            "schedule": [
                "at any time"
            ]
        }
    ],
    "timezone": "Asia/Tokyo"
}

go.sumのpackage sumが抜ける問題

RenovateでpostUpdateOptionsgomodTidy を指定している場合 *2 go mod tidy するときにリソースの都合で依存パッケージを全てはダウンロードしていないために、 go.sum からパッケージのsumが抜けることがあります。

これはかなり困る問題なんですが、Renovate側では今の所対策する予定は無いそうです。ということで、同僚がgo.sumを修正する用のGitHub Actionを作ってくれました。

github.com

このActionを以下のような感じで仕込めば、RenovateのPRに対して、go mod tidyを行って変更があったらコミットしてpushしてくれます.

name: go-mod-fix
on:
  push:
    branches:
      - renovate/*

jobs:
  go-mod-fix:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 2
      - name: fix
        uses: at-wat/go-sum-fix-action@v0
        with:
          git_user: @@MAINTAINER_NAME@@
          git_email: @@MAINTAINER_EMAIL_ADDRESS@@
          github_token: ${{ secrets.GITHUB_TOKEN }}
          commit_style: squash
          push: force

patchバージョンのみをAutomergeしたい場合

デフォルトではminorとpatchは同じブランチで管理されるので、patchのみをアップデートする場合 separateMinorPatch: true が必要。 ということでaws-sdk-goのpatch versionのみをautomergeしたい場合は以下のように書きます。

{
  "packageRules": [
    {
      "packagePatterns": [
        "^github.com/aws/aws-sdk-go"
      ],
      "separateMinorPatch": true,
      "patch": {
        "automerge": true
      }
    }
  ]
}

最後に

色々問題もありましたが、Renovateのおかげで弊社のGoプロジェクトはかなり健全な状態にできてるかと思います。もしRenovateを使おうと検討している場合、参考になれば幸いです。

*1:よくあるのは複数のプロジェクトから参照される共通コアライブラリ的なの

*2:というかこれをしたくない場合ってあるのかな?

Herokuで特定のGoのバージョンを使う

デフォルトはGo buildpackのデフォルトバージョンが使われます (2020/05/31現在1.12.17)

これを変えたい場合以下の方法があります

  • go.mod// +heroku goVersion go1.14 というディレクティブを追加する
  • アプリケーションの環境変数GOVERSION を追加する (例: GOVERSION=go1.14)

go.modgo ディレクティブのバージョンを使うという提案もあるようで、もう少ししたら go.mod のバージョンを使うようになるかもしれません

参考

elements.heroku.com

GitHub Actionsでテストを分割する

やりたいこと

GitHub Actionsでテストケースを分割して、複数ノードで実行する

CircleCIだとプラットフォーム側で対応してくれていますがGH Actionsだと見当たらなかったので同等のことをやるための設定のメモです。

設定例

  rspec:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 2
      matrix:
        ci_node_total: [2]
        ci_node_index: [0, 1]
    steps:
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
      - name: Bundle install
        run: bundle install
      - name: DB setup
        run: bundle exec rake db:setup
      - name: Rspec
        run: |
          ruby -e 'Dir.glob("spec/**/*_spec.rb").each{|f| puts f}' | awk "NR%${CI_NODE_TOTAL}==${CI_NODE_INDEX} {print}" > /tmp/tests-to-run
          cat /tmp/tests-to-run
          bundle exec rspec $(cat /tmp/tests-to-run)
        env:
          CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
          CI_NODE_INDEX: ${{ matrix.ci_node_index }}

GitHub ActionsにはBuild Matrix という機能があって、本来はOSのバージョンと言語のバージョンの複数のマトリクステストを実施するための機能なのですが、これを使って ci_node_totalci_node_index という2つのパラメタを登録します。

  • ci_node_total: ノードの合計, max-parallel と同じ数にする必要がある
  • ci_node_index: ノードのインデックス, 0始まりで ci_node_total-1 で終わる配列

これで、 rspec のJobは (2, 0) と (2, 1) という2つの設定で実施されることになります

f:id:kamatama_41:20200518161853p:plain

あとはindex毎にspec配下のファイルを分割してrspecコマンドに渡すだけです。CircleCIだとsizeの大きさとか各exampleのかかった時間とかでいい感じに平準化してくれますが、GitHub Actionsにはそういう機能は無いので、単にspecファイルを2分割して渡すのみです。

ちなみに、Go言語の場合はこんな感じでテストを分割実行できます

$ go test $(go list ./... | awk "NR%${CI_NODE_TOTAL}==${CI_NODE_INDEX} {print}")

2019年振り返り、2020年の抱負

2019年は書かなかったので、2年ぶりの振り返り記事になります。

転職

退職しました4 - かまたま日記3

一番のトピックはこれでしょうか。前職では色々心残りもありましたが、春にSEQSENSEという警備ロボットを作っているベンチャー企業に転職しました。

現職では主にバックエンド全般を担当していて、GoとかRailsとかTerraformとかを使って仕事しています。ここ数ヶ月は特にロボットのカメラからの動画をWebRTCで配信する機能をメインで見ていて、RTPとかMatroskaとかVP8とかの仕様に詳しくなりました。

去年の8月にロボットの商用化が始まったばかりでまだまだ改善点がたくさんあるので、興味のあるかたはぜひともお声掛けください。

www.wantedly.com

OSS活動

Embulkの内部構造に詳しくなるために、Embulkのプラグインを2つ作ったり

kamatama41.hatenablog.com

kamatama41.hatenablog.com

現職でTravis CIの設定をコード化するためにTerraformのプラグインを作ったりしていました

kamatama41.hatenablog.com

オーナー移管してしまいましたが、tfenvみたいなヒット作をいつかまた作りたいです。

子育て

子供も2歳になり、喋ったり走り回ったりできるようになりました。 週末は行けるときは色んな所に行くようにしているのですが、東京あそびマーレという八王子のアミューズメント施設にはとてもお世話になりました。オススメです。

asobimare.jp

最近は物や動物の名前を覚えたり、数を数えたりできるようになってきたので、動物園や博物館みたいなモノと触れ合う的なところに行く機会を増やしているところです。

麻雀

2018年後半くらいから競技麻雀に猛烈にハマっており、AbemaTVで麻雀の動画を見たり麻雀の本を読み漁ったり都内の某雀荘に足繁く通ったり家でも天鳳やったりと、最近は趣味の時間をほぼ麻雀にベットしている状態です。大会にもいくつか出ているのですが、一番結果が出たのは麻雀最強戦2019で東東京最強位決定戦まで行けたことでしょうか。東東京の試合では準決勝で敗れたのですが、負けた相手が後に最強戦ファイナルまで進出した方だったので、まあ仕方ないかなと。今年は何かしらの結果を出したいです。

2020年の抱負

  • 何かしらの勉強会・カンファレンスで登壇をする
    • 自分の知識向上と会社のプレゼンスアップのため
  • ダイエット (5kg以上減量する)
    • 直近の健康診断で、メタボリック診断されてしまったため。。
  • 天鳳鳳凰卓に行く *1
  • マチュア最強位決定戦に進出する

過去の振り返り

*1:7段になる