Mockitoの`verify`カウント数をリセットする方法

Mockito はとても便利で、たとえばこんな風にするだけで「このメソッドが叩かれたか?」というチェックができる:

MyObj obj = spy(new MyObj());

doNothing().when(obj).firedMyEvent(); // 実際の処理を無視しておく

obj.doSomething(); // 内部で1回だけ`MyObj#firedMyEvent`が叩かれる
verify(obj, times(1)).firedMyEvent(); // ここでassert

よくある利用ケースとして、何回か同じイベントが叩かれることをチェックしたい時がある:

MyObj obj = spy(new MyObj());

doNothing().when(obj).firedMyEvent();
{
    obj.doSomething();
    verify(obj, times(1)).firedMyEvent();
}
// ここで`reset(obj)`すればいいか?
{
    obj.doSomething(); // もう一回呼ぶ
    verify(obj, times(1)).firedMyEvent(); // またここで発火していたことを確認したいが...?
}

しかし上記のテストは失敗する。obj自身はinvocationのカウンタを持ち続けているのでtimes(1)ではなくtimes(2)になってしまうわけだ。
安直に org.mockito.Mockito.reset でリセットしてしまうと、冒頭のdoNothingでmockした箇所までリセットしてしまい面倒だ。

カウンタだけリセットし、また1からカウントするには以下のようにすればよい:

MyObj obj = spy(new MyObj());

doNothing().when(obj).firedMyEvent();
{
    obj.doSomething();
    verify(obj, times(1)).firedMyEvent();
}
clearInvocations(obj); // org.mockito.Mockito.clearInvocations
{
    obj.doSomething();
    verify(obj, times(1)).firedMyEvent(); // OK
}

clearInvocationsの引数はvarargsになっているので、第2、第3と引数へどんどん繋げていって一括リセットできる。

Kotlinで中置記法四則演算to逆ポーランド記法

以前、プログラミング学び直しシリーズとして Kotlinによる逆ポーランド記法四則演算機 を作った。その続きとして、Kotlin/JVMで中置記法の計算式を逆ポーランド記法に変換するコードを書いた。

enum class Operator(
    val value: Char
) {
    ADD('+'),
    SUB('-'),
    MUL('*'),
    DIV('/'),
    BRACKET_OPEN('('),
    BRACKET_CLOSE(')'),
    //
    ;

    fun match(x: Char?): Boolean
            = this.value == x

    fun match(x: String?): Boolean
            = x?.length == 1 && match(x.first())

    fun str(): String
            = value.toString()

}

// `char`がいずれかの演算記号と合致するか
fun Char.isOperator(): Boolean
        = Operator.values().map { it.value }.contains(this)

data class Node(
    val expr: List<String>,
    val left: Node? = null,
    val right: Node? = null
)

fun main(args: Array<String>) {
    val expr = tokenize(args[0])

    val tree = process(
        Node(expr)
    )

    println(
        traversePostOrder(tree).joinToString(" ")
    )
}

fun tokenize(src: String): List<String> {
    val tokens: MutableList<String> = mutableListOf()
    var currentNumbers = ""

    repeat(src.length) { i ->
        val x = src[i]

        if (x.isWhitespace()) {
            return@repeat // 空白は削除
        } else if (x.isOperator()) {
            if (currentNumbers.isNotEmpty()) {
                // 何かしらの記号だった場合、ここまでの連続した数値をtokenとして追加
                tokens.add(currentNumbers)
            }
            currentNumbers = ""
            tokens.add(x.toString()) // 記号を追加
        } else {
            currentNumbers += x // 連続した数値を記録
        }
    }

    // 終端まで来たら残った内容を追加
    if (currentNumbers.isNotEmpty()) {
        tokens.add(currentNumbers)
    }

    return tokens
}

// 再帰で自身より下位の数式を処理
fun process(tree: Node): Node {
    val expr = removeMostOuterBrackets(tree.expr)
    val i = searchLowPriorityOperatorPosition(expr)

    if (i == null) {
        return tree.copy(expr) // 自身より下位が無ければ括弧を削除したものを返し終了
    } else {
        return Node(
            expr = listOf(expr[i]), // 最も優先度が低く右側の記号
            left = process(Node(
                expr.slice(0 .. (i - 1)) // 頭から記号の手前まで
            )),
            right = process(Node(
                expr.slice((i + 1) .. (expr.size - 1)) // 記号直後から終端まで
            ))
        )
    }
}

// 両側の括弧が対応していれば削除する
fun removeMostOuterBrackets(expr: List<String>): List<String> {
    if (!Operator.BRACKET_OPEN.match(expr.firstOrNull()))
        return expr // 最初が括弧でなければ削除する必要が無い

    var nest = 0

    repeat(expr.size) { i ->
        when (expr[i]) {
            Operator.BRACKET_OPEN.str() -> {
                nest++
            }
            Operator.BRACKET_CLOSE.str() -> {
                nest--

                // 最後の文字に来る前に開始した括弧が全て閉じられてしまった場合、両側の削除は不要 (できない)
                if (i != (expr.size - 1) && nest == 0)
                    return expr
            }
        }
    }

    return removeMostOuterBrackets(
            expr.slice(1 .. (expr.size - 2) )
    )
}

// 最も優先順位の低い演算子を探す
fun searchLowPriorityOperatorPosition(expr: List<String>): Int? {
    var ret: Int? = null
    var lastPriority = Int.MAX_VALUE // 優先順位の低いものがほしいため

    var nest = 0
    var priority = 0

    repeat(expr.size) {
        when (expr[it]) {
            Operator.ADD.str(), Operator.SUB.str() ->
                priority = 1
            Operator.MUL.str(), Operator.DIV.str() ->
                priority = 2
            Operator.BRACKET_OPEN.str() -> run {
                nest++
                return@repeat
            }
            Operator.BRACKET_CLOSE.str() -> run {
                nest--
                return@repeat
            }
            else -> run { return@repeat }
        }

        // 優先順位が低く、かつ最も後に登場したものを最後に返す
        if (0 == nest && priority <= lastPriority) { // ネスト0 = 優先順位が低い
            lastPriority = priority
            ret = it
        }
    }

    return ret
}

// 後行順序訪問
fun traversePostOrder(tree: Node): List<String> {
    var ret: MutableList<String> = mutableListOf()

    tree.left?.let { ret.addAll(traversePostOrder(it)) }
    tree.right?.let { ret.addAll(traversePostOrder(it)) }

    return ret + tree.expr
}

たとえば以下のように入力すると:

(((3+3+4)*(334+(3/3-4))*(3*3*4)+(3+3-4))*(3-34)+(3-(3+4)))*(3-34)+(3-(3-4))

以下のように出力される:

3 3 + 4 + 334 3 3 / 4 - + * 3 3 * 4 * * 3 3 + 4 - + 3 34 - * 3 3 4 + - + 3 34 - * 3 3 4 - - +

さて、これを担当している授業で高校生に教えようとしているのだが、どう説明していこうかな。

GitLab.comへのユーザ登録から手元でリポジトリを操作できるようにするまで

様々なバージョン管理システムの中でも特に現代のソフトウェア開発に欠かせない存在となったGit。そしてGitホスティングサービスの中でも最も利用されているのが GitHub.com です。
しかしながらプロジェクトの様々な事情により必ずしもGitHubが用いられることはなく、同等の機能を提供する他のホスティングサービスを用いることは多々あります。

GitHub以外のGitホスティングサービスの中でも特に多機能なもののひとつに GitLab.com というウェブサイトがあります。

gitlab.com

この記事ではプログラミング初学者を対象に、GitLab.com への登録手順を可能な限り丁寧に解説します。
なお、この記事はプログラミング未経験者向けのチュートリアル資料として作成したものを公開用に再構成したものであるため、極度に平易な言葉を選んでいることをご了承頂きたい。

はじめる前に

以下をご用意ください。

  • git, ssh-keygenコマンドが使え、共用ではない、または専用アカウントがあるコンピュータ
  • 今手元で受け取れ、日常的に利用するEメールアドレス (Eメールアカウント、Gmailなど)
  • インターネットに接続された (十分に新しい) コンピュータ

GitLabのユーザ登録画面へアクセスする

まずはウェブブラウザを用いてウェブサイトへアクセスします。
Google等の検索エンジンを用いてgitlabなどと検索するか、直接https://gitlab.com (またはhttps://about.gitlab.com) を開いてください。
以下のリンクからアクセスすることも可能です:

https://gitlab.com

開いたら、Sign In/Registerをクリックしてください。

f:id:S64:20180501143452p:plain:w500

GitLabのユーザ登録画面は、登録済みユーザのサインイン画面と共通になっています。右側Registerタブをクリックすると、会員登録が可能です。

f:id:S64:20180501143730p:plain:w500

情報を入力する

表示されたフォームへ、必要事項を入力します。

f:id:S64:20180501150604p:plain:w500

Full nameはそのままフルネーム... というよりは、表示されるあなたのユーザ名です。私の場合はアルファベットの実名を利用しています。たとえば普段利用しているハンドルネーム、ニックネームなどでも良いでしょう。この内容はインターネット上の誰でも参照できます。

Usernameは、自分の公開するプロフィールやGitリポジトリのURL、他のユーザとやり取りしたりコミットした際に利用されるIDです。ちょうどTwitterのIDのように、@〜〜〜の形式でメンションが届いたりするかもしれません。この内容はインターネット上の誰でも参照でき、GItLab上で一意である (唯一の組み合わせである) 必要があります。

EmailおよびEmail confirmationは、このユーザ登録における本人確認, 他ユーザと共同開発した際の通知受信, Gitのコミットとユーザの紐付けなどで利用されるEメールアドレスです。この内容は形式としては公開 / 非公開が任意ですが、Gitの仕組み上コミット作成時にEメールアドレスを入力する必要があるため、コミットログを参照する際に実質的に公開されます。注意事項として、Gitで利用するメールアドレスと同一のものを設定してください。

Passwordはサインインなどで利用するパスワードです。8文字以上で任意の文字列を指定できます。

I'd like to receive updates via email about GitLab.にチェックを加えると、サービス利用上の通知以外にもGitLabそのものの更新等についてお知らせを受け取ることができます。

最後にreCAPTCHAでボットでないことを確認し、Registerで内容を送信します。

メールで本人確認をし、サインインする

Registerをクリックすると、Almost there...と書かれたページが表示されます。これはメールアドレスによる本人確認を要求するものです。
登録したEメールアドレスに対応するメールボックスを確認し、認証をしてください。なお、表示される緑色のボタンは「確認メールを再送する」ものであるため、メールを紛失しない限りはクリック不要です。

GitLabよりConfirmation instructionsというメールが届いています。メール内のConfirm your accountをクリックして、認証をしてください。

f:id:S64:20180501151453p:plain:w500

確認に成功するとその旨が表示と併せさきほどのログイン画面に転送されます。こちらではSign inタブを選択し、さきほど入力した内容を用いてSign inをします。

f:id:S64:20180501151852p:plain:w500

これで会員登録が完了しました。

f:id:S64:20180501152043p:plain:w500

必要なら: メールアドレスを追加する

GitLabはプライベート, 仕事, 学校, サークルなど様々なシーンで利用する可能性があります。その場合自分が利用するコンピュータ, 名義が複数あり、アカウントが分散してしまう可能性があります。
GitLabではそのようなことを防ぐため、ひとつのアカウントに複数のメールアドレスを紐付けることができます。
事前にお持ちのメールアドレスをまとめて追加しておくことをお勧めします。

f:id:S64:20180501154422p:plain:w500

SSHキーを生成する

GitではHTTP over TLSまたはSSHのいずれかを通して通信するのが一般的です。今回はSSHでの通信を可能にするための手順を説明します。
コンピュータ上でterminal、つまり端末エミュレータを開いてください。

以下のフォーマットを参考に、コマンドを実行します。

ssh-keygen -t rsa -b 4096 -C 'Gitで利用するメールアドレス' -f ~/.ssh/gitlab_rsa

-Cオプションはコメントを意味し、メールアドレスを入力するのが一般的です。
-fオプションはファイルの保存先を指定し、一般的にユーザのホームディレクトリ直下.ssh/内に保存し、以上のようなサービス名+暗号化方式というフォーマットでファイル名を命名しているように感じます。

フォーマットに当てはめると、たとえば

ssh-keygen -t rsa -b 4096 -C 'myname@example.com' -f ~/.ssh/gitlab_rsa

のようになるでしょう。
コマンドを実行すると、

Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):

などのように表示されます。ここでは生成するキーをパスワードで暗号化することができます。
パスワードに設定すべき文字列はこちらの外部の記事が大変参考になります。
空欄にした場合、暗号化をしません。

Enterで確定するとEnter same passphrase again:と出るため、確認として再度同じパスフレーズ (空欄なら空欄で) を入力します。

Your identification has been saved in /home/コンピュータのユーザ名/.ssh/gitlab_rsa.
Your public key has been saved in /home/コンピュータのユーザ名/.ssh/gitlab_rsa.pub.

などと表示されたら、生成完了です。

注意: 鍵があればアカウント認証できる

この鍵が保存されていれば、またコピーすることができれば、どんな人物でもあなたのGitLabアカウントでリポジトリを操作することができてしまいます。
くれぐれも共用コンピュータの共用ユーザでは追加予定の鍵を作成しないでください。最低でも、コンピュータ内に自分専用のアカウントを作成し、他のアカウントからファイルを参照できないようにしてください。

特定ホストで鍵が利用されるよう設定する

SSHではどのホストへの接続かにより利用する鍵を切り替えられます。
今回はGitLab向けの内容です。~/.ssh/configというファイルを開き、以下のように設定します。

Host gitlab
  HostName gitlab.com
  IdentityFile ~/.ssh/gitlab_rsa
  AddKeysToAgent yes
  User git
  Port 22

公開鍵をコピーし、GitLabへ追加する

sshのキーには 秘密鍵と公開鍵 があります。ファイル名の末尾に.pubと付いているのが公開鍵です。こちらをGitLabへ登録します。
以下のコマンドを実行すると、公開鍵の内容が出力されます。

cat ~/.ssh/gitlab_rsa.pub

これをクリップボードにコピーします。ssh-rsaから始まり、コメント (メールアドレス) が書かれた部分まで全体です。

GitLabの設定を開き、SSH Keysというメニューをクリックします。
こちらのKeyにさきほどコピーした公開鍵をペーストし、Titleにはその鍵を識別する名前を入力します。

f:id:S64:20180501164838p:plain:w500

たとえば私の場合、MacBook Pro 15"上のUbuntu用の公開鍵のため、画像のようにしました。
入力したら、Add keyで完了です。

完了

以上で手元のコンピュータからGitLabを利用する準備ができました。
この後には必要に応じてリポジトリを作成したり、手元にcloneしたり、GitLabへコードをpushしたりという具体的な利用がありますが、この記事では割愛します。

SDKMANのJDKをIntelliJ IDEAで使う

JVM言語のSDKをSDKMANというツールで異なるバージョンと共存・管理するのが流行っているように感じます。
しかしながらこのような実行環境管理ツールは OSごとに整備された通常のSDK配置とは異なるため、IntelliJ IDEAなどのIDEは自動的に検索することができません。

www.jetbrains.com

この記事では、SDKMANで配置されたJava Development KitをIntelliJ IDEAから利用するための手順をプログラミング初学者でも理解できるよう可能な限り丁寧に記します。
余談ですが、SDKMANの正式名称は "SDKMAN!" みたいです。この記事では基本省略しています。

そもそも初学者がSDKMANを利用するケースは (私の生徒たち以外に) 存在するのでしょうか...?

IntelliJ IDEAでプロジェクトの標準設定を開く

IntelliJ IDEAが利用するSDKは、IDEA上で「プロジェクトの標準設定」として直接設定する必要があります。起動時の画面下部にある Configure より、Project Defaults > Project Structure と進み、クリックし開きます。

f:id:S64:20180430175757p:plain

SDKを追加する

Default Project Structureというウィンドウが開かれたら、左側ペインのPlatform Settings内のSDKsをクリックします。
右側ペインにの上部にあるプラスボタン (Add New SDK) をクリックし、JDKをクリックします。

f:id:S64:20180430180425p:plain

ディレクトリを指定する

ここからは利用しているプラットフォームにより異なります。

Ubuntu等Linuxディストリビューションの場合 通常SDKMANによるJDKは/home/<yourusername>/.sdkman/candidates/java内で管理されています。
.sdkmanディレクトリが隠しファイルであるため、表示してあげると指定しやすくなります。

ファイルピッカー上に「隠しファイルを表示する」ボタンがあるためそれをクリックし有効にし、ファイルツリー上で上記ディレクトリを辿ります。
ディレクトリ内にインストール済みJDKが表示されるため、current以外からIDEA上で利用したいものを選択し、OKしてください。

f:id:S64:20180430181823p:plain

macOSの場合

通常SDKMANによるJDKは/Users/<yourusername>/.sdkman/candidates/java内で管理されています。
.sdkmanディレクトリが隠しファイルであるため、表示してあげると指定しやすくなります。

ファイルピッカーのウィンドウ上でCmd + Shift + .のショートカットキーを入力すると、隠しファイルが表示されます。この状態のファイルツリー上で、上記ディレクトリを辿ります。
ディレクトリ内にインストール済みJDKが表示されるため、current以外からIDEA上で利用したいものを選択し、Openしてください。

f:id:S64:20180430182816p:plain

今回の場合、sdk install javaで自動的に選択, インストールされたJDKが8.0.163-zuluだったため、これを選択しています。

f:id:S64:20180430183031p:plain

OKをクリックし、ウィンドウを閉じます。
以上でSDKMANで追加したJDKを IntelliJ IDEAに認識させることができました。

プロジェクト作成時にJDKを選択する

以降はJDKの追加手順から離れるため、おおまかな手順のみの解説をします。
Create New Projectなどの項目からプロジェクトを作成し、Javaのテンプレートを選択した場合を想定します。

この際に右側ペインに表示されるProject SDKという項目で、さきほど追加したJDKを選ぶことができます。

f:id:S64:20180430183513p:plain

開発するプロジェクトで適切なJDKを選択し、Nextで先へ進めればよいです。

新たにSDKMANで追加したJDKを認識させる

それ以外にもSDKMAN (またはそれ以外) でインストールしたJDKがある場合、または今後新たにJDKを追加した場合、同じ手順を繰り返すことでIDEAへ認識させることができます。それらを利用する際は、プロジェクト作成ウィザード上のProject SDKから選択することができます。

gnome-shellのfavoritesアプリをCLIから操作するアプリ "gnome-favorites-edit" を公開しました

gnome-shellのDash (Macで言うDockみたいなやつ) 上ではアプリケーションをピン留めすることができ、これを「favorite-apps」といいます。
これはアプリ一覧上表示されているもの、つまり.desktopファイルが登録済みであれば任意のアプリケーションを追加することができるのですが、制約としてGUIであるgnome-shell上でしか操作ができません。

多くのユースケースではこれで問題ありませんが、たとえば

  1. 複数台Ansible等を用い一括でセットアップする
  2. インストール完了時に自動で追加する

などを行いたい場合には不便です。特に今回は1が問題になりました。

私は講師業をしており、担当している授業にて初学者向けにLinuxデスクトップ環境を利用しようとしたものの、慣れないGUI上でアプリケーションを探してもらうのは難しいため、できるだけ少ないステップ数でアプリケーションを起動できるようにしたかったものの、十数台にわたるコンピュータすべてで操作をするのは困難でした。

作ったもの

そこでgnome-shell上のfavorite-appsをCLIから操作できる "gnome-favorites-edit" というコマンドを作りました。

github.com

気に入ったらStarを付けてください。

Star

使い方

gnome-favorites-edit {put, remove} app.desktop という形式で実行すると、Dash上のアプリケーションを操作することができます。
また --ignore-errors というオプションを付加すると、既に追加済み / 削除済み だった場合のエラーを抑止することができます。これはAnsibleなどで用いる場合に便利です。

インストール方法

GitHub上のReleasesに.debおよび.rpm形式のパッケージが用意されており、これをダウンロードし直接インストールできます。

# on Ubuntu
sudo apt-get install -f ./gnome-favorites-edit_0.0.1-1_all.deb
# or Fedora
sudo dnf install ./gnome-favorites-edit-0.0.1-1.noarch.rpm

対応OS

ここ最近の多くのFedora、またはUbuntu 18.04 LTSなどがgnome-shellを採用しており、またそれらで動作確認をしています。

ライセンス

Apache License 2.0 です

F# でFizzBuzz

F#でFizzBuzzを書く。

let fizz x = x % 3 = 0
let buzz x = x % 5 = 0
    
let fizzBuzz x =
    match x with
        | _ when ( (fizz x) && (buzz x) ) -> "FizzBuzz"
        | _ when (fizz x) -> "Fizz"
        | _ when (buzz x) -> "Buzz"
        | _ -> string x

seq {1 .. 100}
    |> Seq.iter (fun x -> printfn "%s" (fizzBuzz x))
    |> ignore

Haskellを事前に学んでおいたおかげで、F#も比較的すぐに書きはじめられた。

Kotlinで逆ポーランド記法四則計算機

真面目じゃない方のブログでやっているプログラミング学び直し日記シリーズ。Kotlin (Kotlin/JVM) で逆ポーランド記法 (Reverse Polish Notation) 計算機の実装。自然数による四則計算のみ対応。少数とか使えない。

import kotlin.text.Regex
import java.util.Stack

enum class Oprs(val str: String, val exec: (x: Int, y: Int) -> Int) {
    Add("+", { x, y -> x + y}),
    Sub("-", { x, y -> x - y}),
    Mul("*", { x, y -> x * y}),
    Div("/", { x, y -> x / y})
}

fun main(args: Array<String>) {
    val xs = args[0].split(Regex("\\s+"))
    val ret = Stack<String>()

    for (x in xs) {
        when {
            Regex("\\d+").matches(x) -> {
                ret.push(x)
            }
            Oprs.values().map { it.str }.contains(x) -> {
                val snd = Integer.parseInt(ret.pop())
                val fst = Integer.parseInt(ret.pop())

                ret.push(Integer.toString(
                    when (x) {
                        Oprs.Add.str -> Oprs.Add.exec(fst, snd)
                        Oprs.Sub.str -> Oprs.Sub.exec(fst, snd)
                        Oprs.Mul.str -> Oprs.Mul.exec(fst, snd)
                        Oprs.Div.str -> Oprs.Div.exec(fst, snd)
                        else -> throw IllegalArgumentException()
                    }
                ))
            }
            else -> throw IllegalArgumentException()
        }
    }

    if (ret.size != 1) throw IllegalArgumentException()

    System.out.println(ret.pop())
}