JUnit5をmainから実行する

こんにちは。akaimoです。
今回はJUnit5をmain関数内で使用する方法です。

環境

  • Java9
  • JUnit5.1.0-M1
  • Kotlin1.2

ソース

これでmainからJUnitを動かせます。詳しい解説は後ほど。

import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder
import org.junit.platform.launcher.core.LauncherFactory
import org.junit.platform.launcher.listeners.SummaryGeneratingListener
import org.junit.platform.engine.discovery.ClassNameFilter.*
import org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage
import java.io.PrintWriter


fun main(args: Array<String>) {
    val launcher = LauncherFactory.create()
    val summary = SummaryGeneratingListener()
    launcher.registerTestExecutionListeners(summary)
    val request = LauncherDiscoveryRequestBuilder
            .request()
            .selectors(selectPackage("jp.hoge.piyo.test"))
            .filters(includeClassNamePatterns(".*"))
            .build()
    launcher.execute(request)
    summary.summary.printFailuresTo(PrintWriter(System.out))
    summary.summary.printTo(PrintWriter(System.out))
}

これを実行すると次のようなログが出力されます。

Test run finished after 13255 ms
[         3 containers found      ]
[         0 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         4 tests found           ]
[         3 tests skipped         ]
[         1 tests started         ]
[         0 tests aborted         ]
[         1 tests successful      ]
[         0 tests failed          ]

もしテストが失敗していたら、このログの前に失敗したテストと失敗した内容が表示されます。

詳細

testディレクトリ配下に入れIDEなどでテストを実行する場合はテストコードだけを書けば良いですが、main内で実行させる場合は全て自分で実行させなければいけません。

Launcher

JUnit5には、テストを検出したりフィルタリング、実行するためにLauncherというものを使用します。
まずはそのLauncherを作成します。

val launcher = LauncherFactory.create()

Summary

次はSummaryです。
これは名前の通り、テストの内容であったり結果を保持しています。
Summaryを作成して、Launcherに登録します。

val summary = SummaryGeneratingListener()
launcher.registerTestExecutionListeners(summary)

Request

Requestでは実行するテストの選択やフィルタリングをします。
selectorefiltersを適宜変更して実行したいテストが実行できるようにします。

val request = LauncherDiscoveryRequestBuilder
            .request()
            .selectors(
                selectPackage("jp.hoge.piyo.test"),
                selectClass(MyTestClass::class.java)
            )
            .filters(includeClassNamePatterns(".*Tests"))
            .build()

作成したrequestをLauncherに渡せばテストを実行できます。

launcher.execute(request)

結果の表示

最後にテスト結果を表示して終了です。

summary.summary.printFailuresTo(PrintWriter(System.out))
summary.summary.printTo(PrintWriter(System.out))

なお、summary.summaryよりデフォルトで出力される項目に個別にアクセスできるので、任意のフォーマットで出力することもできます。

react-reduxにflowtypeを導入しPropsに型を付ける

flowtypeを導入したとき、reactとreduxをつなぐ部分の情報が少なかったのでまとめます。

flowtypeの導入前

比較として導入前のソースを載せておきます。

import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux'
import * as actions from '../actions

class SamplePage extends Component {
  render() {
    const {title, description, myAction} = this.props

    return (
      <div>
        <button onClick={myAction}>{title}</button>
        <p>{description}</p>
      </div>
    )
  }
}

SamplePage.propTypes = {
  title: PropTypes.string,
  description: PropTypes.string,
  myAction: PropTypes.func
}

const mapStateToProps = state => {
  return {
    title: state.title,
    description: state.description
  }
}

const mapDispatchToProps = dispatch => {
  return {
    myAction: () => dispatch(actions.myAction())
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(SamplePage)

導入後

これが導入後のソースです。propTypesが消えましたが、typeを記述しているので行数はあまり変わっていません。

import React, {Component} from 'react'
import {connect} from 'react-redux'
import * as actions from '../actions
import type {MapStateToProps, MapDispatchToProps} from 'react-redux'

type StateToProps = {
  title: string,
  description: string
}

type DispatchToProps = {
  myAction: () => void
}

type Props = StateToProps & DispatchToProps

class SamplePage extends Component<void, Props, void> {
  render() {
    const {title, description, myAction} = this.props

    return (
      <div>
        <button onClick={myAction}>{title}</button>
        <p>{description}</p>
      </div>
    )
  }
}

const mapStateToProps: MapStateToProps<*, *, StateToProps> = state => {
  return {
    title: state.title,
    description: state.description
  }
}

const mapDispatchToProps: MapDispatchToProps<*, *, DispatchToProps> = dispatch => {
  return {
    myAction: () => dispatch(actions.myAction())
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(SamplePage)

ポイント

propTypesが3つのtypeに置き換えられています。
その中で特に大事なところが以下で、

type Props = StateToProps & DispatchToProps

mapStateToPropsmapDispatchToPropsで生成された2つのオブジェクトのtypeが合成されたものが、

class SamplePage extends Component<void, Props, void> { ...

となりcomponentに渡されていることを明示的に示すことができます。

補足1

MapStateToPropsは以下のように定義されています。

declare type MapStateToProps<S, OP: Object, SP: Object> = (state: S, ownProps: OP) => SP;

ジェネリクスで、SとOPを受け取ってSPを返す関数という型になってますね。
導入後のソースではMapStateToProps<*, *, StateToProps>のように宣言しています。 Stateのtypeを宣言していれば、MapStateToProps<State, *, StateToProps>となり、MapStateToProps内のstateに対して型が適用されるのでSやOPに対しても型の宣言をすることをオススメします。

補足2

2018/02/01現在で最新のflowtypeではComponentに渡すPropsの型を以下のように記述します。

class SamplePage extends Component<Props> { ...

Travis CIでfastlaneを使用する

こんにちは。akaimoです。
Travis CIを利用しているiOSプロジェクトにfastlaneを導入して、xcodebuildを直接実行する環境から開放されたので、
備忘録としてやったことを残しておきます。

github.com

前提

  • CI上でfastlaneを使用
  • 複数人で開発

fastlaneのインストール

まずはfastlaneをインストールします。

fastlaneはgemで公開されているのでrubyの環境のバージョンを固定しておきましょう。
CIにインストールされているrubyのバージョンと同じにしておくことをオススメします。
違うとCI上でrubyのインストールが始まってしまうかもしれません。無駄な時間なので素直に同じバージョンを使いましょう。

$ rbenv local 2.4.2

こんどはfastlaneのバージョンを固定するためにbundlerをインストールします。

$ gem install bundler

Gemfileに以下を記述します。

source 'https://rubygems.org'

gem 'fastlane'

インストールします。

$ bundle install --path=vendor/bundle

ポイントとしては --path を指定することでローカルの環境を汚すことなくfastlaneをインストールできます。
ここでインストールされたディレクトリはgitで管理する必要はないので.gitignoreに入れておきましょう。

セットアップ

xcodeprojがあるディレクトリでinitコマンドを実行して雛形を作成します。

$ bundle exec fastlane init

実行するとlane(1つのコマンドで実行されるアクションをまとめたもの)を作成するか聞かれます。2か3を選ぶと質問に答えるだけでitunes connectの設定がされるのでオススメです。

完了するとfastlaneというディレクトリが作成されています。
ファイルの中は以下のようになっています。

  • Fastfile
    • レーンを記述します。initコマンドで選択したlaneが作成されています。
  • Appfile
    • fastlane全体で使用する情報を記載します。initで2か3を選んでいるとapple idやteam idなどが記載されています。

iTunes Connect(TestFlight)へのアップロード

TestFligntへアップロードします。
プロジェクトの設定によって変わりますが、基本は以下のような形です。

  lane :beta do
    build_app(
      workspace: "Hoge.xcworkspace",
      scheme: "Hoge",
      output_directory: "./tmp"
    )
    upload_to_testflight(ipa: "./tmp/Hoge.ipa")
  end

lanebetaを実行します。

$ bundle exec fastlane beta

initのときに2か3を選択した場合は、initで入力した情報がキーチェーンに保存されているのでローカル環境ではアップロードできますが、
CI上だとapple idのパスワードが存在せず失敗してしまいます。

このような時のためにfastlaneはFASTLANE_PASSWORDという環境変数からパスワードを取得してログインするようになっています。
.travis.yml環境変数にパスワードをセットしましょう。

この記事が参考になります。

qiita.com

これでCI上からアップロードできるようになります。

HockeyAppへのアップロード

次はHockeyAppへアップロードします。

  HOCKEY_API_TOKEN = ENV['HOCKEY_APP_TOKEN']
  PLIST_PATH = "path/to/Info.plist"
  build = get_info_plist_value(path: PLIST_PATH, key: "CFBundleVersion")
  version = get_info_plist_value(path: PLIST_PATH, key: "CFBundleShortVersionString")

  lane :alpha do
    build_app(
      workspace: "Hoge.xcworkspace",
      scheme: "Hoge",
      configuration: "Adhoc",
      export_method: "enterprise",
      output_directory: "./tmp"
    )
    hockey(
      api_token: HOCKEY_API_TOKEN,
      bundle_short_version: version,
      bundle_version: build,
      ipa: "./tmp/Hoge.ipa",
      dsym: "./tmp/Hoge.dSYM.zip",
    )
  end

先程と同じようにapiのtokenを環境変数にセットし、それを読み込みます。
TestFlightへのアップロードができていれば、同じようにできるはずです。

Unit Testの実行

せっかくCIの環境があるので、テストも実行しておきましょう。

  lane :test do
    run_tests(
      workspace: "Hoge.xcworkspace",
      scheme: "Hoge",
      configuration: "Debug",
      device: "iPhone 8"
    )
  end

Slackへの通知

fastlaneにはslackに通知を送る機能があり、自由にカスタマイズでき、好きなタイミングで送ることができます。
今回はアップロードが完了したら通知するようにします。

まずは通知用のlaneを作成します。

  lane :hockey_notification do
    link = lane_context[SharedValues::HOCKEY_DOWNLOAD_LINK].to_s
    name = sh("echo $USER")

    slack(
      slack_url: "https://hooks.slack.com/services/",
      channel: "#general",
      success: true,
      default_payloads: [],
      attachment_properties: {
        title: "Uploaded to HockeyApp",
        text: "Beta version is now available for <" + link +  "|download>.",
        fields: [
          {
            title: "Build by",
            value: name,
            short: true
          },
          {
            title: "Build Date",
            value: Time.new.to_s,
            short: true
          }
        ]
      }
    )
  end

これを実行すると、このような通知がきます。

f:id:akaimo3:20180120121916p:plain

slackのattachment apiをカバーしているので、公式のドキュメント通りにカスタマイズできます。

api.slack.com

これを先程のHockeyAppへアップロードするlaneに追加します。

  HOCKEY_API_TOKEN = ENV['HOCKEY_APP_TOKEN']
  PLIST_PATH = "path/to/Info.plist"
  build = get_info_plist_value(path: PLIST_PATH, key: "CFBundleVersion")
  version = get_info_plist_value(path: PLIST_PATH, key: "CFBundleShortVersionString")

  lane :alpha do
    build_app(
      workspace: "Hoge.xcworkspace",
      scheme: "Hoge",
      configuration: "Adhoc",
      export_method: "enterprise",
      output_directory: "./tmp"
    )
    hockey(
      api_token: HOCKEY_API_TOKEN,
      bundle_short_version: version,
      bundle_version: build,
      ipa: "./tmp/Hoge.ipa",
      dsym: "./tmp/Hoge.dSYM.zip",
    )
    hockey_notification
  end

alphalaneを実行するとアップロード完了後にslackへ通知がきます。

おわりに

fastlaneを使用してCI上の処理を簡略化しました。
xcodebuildを直接実行していたときと比較すると、

  • CIのコードが大幅に減った
  • 出力に色が付き整形されるので見やすい

ことがすぐに感じられたメリットです。

Xcodeのバージョンアップによるxcodebuildの変更への追随が、fastlaneの更新だけで済むようになればいいな
と期待しながらCIを拡充していきたいと思います。

ScrollViewとStackViewを組み合わせる

akaimoです。
そろそろiOS8のサポートが終わってきたと思うので、TableViewの変わりにStackViewを使う方法を書きます。

使いにくいScrollView

スクロールが必要な複雑な画面の場合、同じ要素が1つも存在しないのにTableViewを使うことがありました。
ScrollViewではなく、TableViewを使う理由はいくつかありますが、
一番大きな理由はScrollViewを使うためのハマりポイントがたくさんあったからだと思います。

そんなScrollViewですが、StackViewと組み合わせて使うことで大幅に使いやすくなります。

ScrollViewの設定

storyboardとxibを使用して実装します。

storyboard

シンプルにいくために、UIViewControllerを継承したcontrollerを用意します。
このcontrollerをstoryboardで設定していきます。

  • このcontrollerにScrollViewを設置し、alignを0で四カ所に制約を付けます。
  • ScrollViewの上にStackViewを設置し、こちらもalignを0で四カ所に制約を付けます。
  • ScrollViewとStackViewを選択し、Equal Widthの制約を付けます。

これで基本的な設定は完了です。

StackView

StackViewの設定をします。

  • AxisをVertical
  • AlignmentをFil
  • DistributionをEqual Centering
  • Spacingを0

view

複数の要素から構成されるので、カスタムviewを使うことをオススメします。

xibを使用してviewを構成します。
viewはいつも通りに作成します。注意するところとしては、このview全体のサイズが決まるように制約を付けることです。
これを守ればあとはStackViewがそのサイズを確保してくれるので、TableViewのように様々なviewを組み合わせてScrollViewを使用することができると思います。

VimにRustの環境を構築する

RustのIntroductionをやってみる途中で、補完とかが効かないとツライなと思ったので
VimにRustの環境を作ってみた。

前提

OSはMacです(たぶんLinuxでも動くと思います)
rustupでRustをインストールしていることを前提とします。

できるようになること

インストール

cargoでRacer(コード補完)とrustfmt(フォーマッタ)をインストールします。

cargo install racer
cargo install rustfmt

Vim Plugin

このPluginを使います。

.vimrc

私の設定を乗せておきます。
使用していない機能もあるので、詳しくは各Pluginのドキュメントを参照してください。

NeoBundle 'racer-rust/vim-racer', {'autoload': {'filetypes': ['rust']}}
let s:hooks = neobundle#get_hooks("vim-racer")
function! s:hooks.on_source(bundle)
  let g:racer_cmd = "$HOME/.cargo/bin/racer"
endfunction

NeoBundle 'rust-lang/rust.vim', {'autoload': {'filetypes': ['rust']}}
let s:hooks = neobundle#get_hooks("rust.vim")
function! s:hooks.on_source(bundle)
  " save時にオートフォーマットする
  let g:rustfmt_autosave = 1
  let g:rustfmt_command = '$HOME/.cargo/bin/rustfmt'
endfunction

NeoBundle 'scrooloose/syntastic', {'autoload': {'filetypes': ['rust']}}
let s:hooks = neobundle#get_hooks("syntastic")
function! s:hooks.on_source(bundle)
  " save時にシンタックスチェックする
  let g:syntastic_mode_map = { 'mode': 'passive', 'active_filetypes': ['rust'] }
  let g:syntastic_rust_checkers = ['rustc', 'cargo']
endfunction

この設定でsave時にオートフォーマットとシンタックスチェックが走ります。
コード補完はC-xC-oでできます。

余談的な

これで結構快適にRustが書けるようになります。
大規模のソースを読むとなると、タグジャンプとかしたくなる。
もう少しRustを書くようならタグジャンプの設定もしようかな。

mackerelでhubotを監視する

raspberry piでhubotを運用していると、いつの間にかhubotが突然死していることがあります。
botが動いてくれないと色々と困るので、先日導入したmackerelで監視して、死亡したらslackにアラートが飛ぶようにします。

観測用のスクリプトを作成

mackerelでカスタムな値を監視するには、自分で監視用のスクリプトを用意する必要があります。

#!/bin/bash

name="process.count.hubot"
monitor_time=`date +%s`
count=`ps aux | grep node | grep hubot | grep -v grep | wc -l`
echo -e "${name}\t${count}\t${monitor_time}"

hubotが生きている = hubotのプロセスが存在する。
と定義して、hubotのプロセス数をmackerelに監視させます。
細かな仕様は以下を参照してください。

ホストのカスタムメトリックを投稿する - Mackerel ヘルプ

mackerel-agent.confに設定

先ほど作成したスクリプトをmackerelに読み込ませます。
mackerel-agent.confに以下のように追記します。

[plugin.metrics.process]
command="/hoge/huga/process.sh"
type="metric"

Agentの再起動

変更を反映させるために再起動します。

$ sudo service mackerel-agent start

以上でhubotの監視は完成です。
あとは、mackerelでアラートと通知先の設定をするだけです。

Raspberry PI 2にLet's Encryptとnginxでhttps環境を構築

Let's Encryptを使ってhttps環境を構築してみたいと思います。

最新のnginxをインストール

せっかくなので、最新のnginxをインストールしてhttp2にも対応させたいと思います。
基本的には以下の記事を見ればできます。

qiita.com

私の環境では、nginxの設定ファイルは/etc/nginx/conf.d/*****.confとしました。
以下では、この記事ではあまり触れられていないLet's Encryptのセットアップを中心に書いていきます。

Let's Encryptのインストール

まずは、Let's EncryptをGitHubからクローンしてきます。

git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt

証明書の発行

上記の記事ですでにnginxのセットアップは完了しているので、--webrootオプションを使って
サーバーを起動したまま取得します。

./letsencrypt-auto certonly --webroot -d ${DOMAIN} --webroot-path ${WEBROOT}

-dオプションをつけるとマルチドメイン証明書となるので、複数ドメインを指定できます。
${WEBROOT}にはnginxで指定したドキュメントルートを指定します。
成功したら、以下のような出力があります。

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/******.**/fullchain.pem. Your cert
   will expire on 2016-**-**. To obtain a new version of the
   certificate in the future, simply run Let's Encrypt again.
 - If like Let's Encrypt, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

この後、nginx -s reloadでnginxを再起動したらhttpsができるようになっているはずです。

以上で完成です。
ずいぶんお手軽にhttpsが利用できるようになりました。
これを機に個人のwebサーバーもhttpsで運用していきたいところです。