mike-neckのブログ

JavaかJavaFXかJavaEE(なんかJava8が多め)

Gradleでプラグインを利用する方法

今更なタイトルですが…

incubatingなAPIで plugins DSLと呼ばれている方法です。plugins DSLはgradleのplugin repositoryにあるプラグインをid(とバージョン)だけで引っ張ってこれるようにする方法で、gradleの2.10くらいに登場しています。gradleにデフォルトに添付されているプラグインとgradle のplugin repository にあるプラグイン以外をこのDSLで指定する方法は登場した当初はなかったのですが、最近はsettings.gradleで頑張ればできるということ(そして、multipleなプロジェクトになるとすごく便利になるということ)を教えてもらったので、忘れてもいいようにメモしておきます。


実例としてJUnit5のプラグイン org.junit.platform.gradle.plugin を使います。

plugins ブロックの中に適用するプラグインのidを記入します。
plugins {
  id 'java'
  id 'org.junit.platform.gradle.plugin'
}

gradleの推奨するプラグインの名前はパッケージ名と同じような名前である id.namespace + id.name のようです。JUnit5のプラグインの場合ですと、 org.junit.platform.gradleid.namespacepluginid.name です。雑に言えば、最後のドットから右が id.name 左が id.namespace です。

org.junit.platform.gradle.plugin は gradle plugin repository にはないので、プラグインに使われるartifactの解決をしなければなりません。artifactを解決する方法として pluginManagement DSLがありそれを settings.gradle ファイルに記述します。
pluginManagement {

}

pluginManagement DSL内部では二つのブロックを書けます。一つは repositories ブロックであり、こちらは名前からわかるようにレポジトリーのURLを記述していきます。もう一つは resolutionStrategy であり、こちらにはプラグインの名前からどのartifactを使うかを記述します。

repositories ブロックには1つのブロックと1つのメソッドが提供されています。 maven ブロックではgradle plugin repository以外のレポジトリーのURLを記述します。 gradlePluginPotal メソッドを呼び出すと gradle plugin repository を使う宣言ができます。gradle plugin repository が不要な場合は gradlePluginPotal を呼び出さなければよいです。なお、maven centralやjcenterなどのおなじみのメソッドが生えていないので、それぞれのURLを書かなければなりません。JUnit5の例では、プラグインのjarはmaven centralにあるので、maven ブロックの中でmaven central のURLを指定します。
repositories {
  maven {
    url ('http://repo1.maven.org/maven2/')
  }
  gradlePluginPortal()
}
resolutionStrategy ブロックの中に eachPlugin ブロックを書いて、その中でプラグインのid(およびバージョン)とその扱いを決定します

requested というプロパティに指定したプラグインのidなどの識別する情報が含まれています。 requested の中には3つのプロパティ(idmoduleversion)が含まれていますが、多くの場合は id 以外は null です。 requested.id の中には次のプロパティが含まれています。

  • id: String - id の文字列表現
  • name: String - idname(最後のドットから右)
  • namespace: String - idnamespace(最後のドットから左)

これを if 文などでマッチングさせて、 eachPlugin ブロックに生えている useModule メソッドから artifact を指定します。artifact の指定方法は dependencies ブロックでの指定方法と同じです。JUnit5の例ではプラグインのidが org.junit.platform.gradle.plugin の場合に org.junit.platform:junit-platform-gradle-plugin:1.0.0 を使いたいので、そのようなマッチングと useModule の指定をします

resolutionStrategy {
  eachPlugin {
    if (requested.id.id == 'org.junit.platform.gradle.plugin') {
      useModule 'org.junit.platform:junit-platform-gradle-plugin:1.0.0'
    }
  }
}

以上の内容を合わせると次のようなスクリプトができます

pluginManagement {
  repositories {
    maven {
      url 'http://repo1.maven.org/maven2/'
    }
  }
  resolutionStrategy {
    eachPlugin {
      if (requested.id.id == 'org.junit.platform.gradle.plugin') {
        useModule 'org.junit.platform:junit-platform-gradle-plugin:1.0.0'
      }
    }
  }
}

なお、 pluginManagement ブロックは settings.gradle の中で一番最初に書かないといけないようです


ここまで試した感じですが、わりと面倒でしたが、 subprojects ブロックで新しいプラグインを読み込ませる方法などを考えると、致し方ないのかなぁという気持ちになります

あと、途中文字が太くなってたりしますが、個々のタイトルを考えるのが面倒で、すべてh5で記述していました…

git secretsとJavaのserialVersionUIDの相性が悪い

念のためにgit secretsを入れて、aws providerを設定していますが、ちょくちょくJavaのプロジェクトのコミットで失敗が発生してた。

コミットのエラーログを調べると、serialVersionUID private static final long serialVersionUID = -8419924062942848690L;aws providerのデフォルトの禁止パターン [A-Z0-9]{20} と一致していることが原因のよう。

ドキュメントをざっと読みましたが、特定のファイルだけ除外するとか、パターンに当てはまったものをさらにパターンにかけるみたいな方法がないので、プロジェクトのルートに .gitallowed ファイルを用意して、

[0-9]{19}L

というパターンを追加した。

これで、とりあえずコミットできるようになった。

JUnit5の標準のassertとDynamicTestの用い方

JUnit5でわりと便利だと思っているのが、JUnit5に標準でついてくる Assertions#assertAll@TestFactory で返す Iterable<DynamicTest> です。


エンタープライズな現場でよく見かけるテストとして、こういうのがあるかと思います。

@Test
void firstTest() {
    final Map<Long, UserEntity> map = getUsers();

    final UserEntity u1 = map.get(1L);
    assumeTrue(u1 != null);
    assertEquals(1L, u1.getId());
    assertEquals("ユーザー1", u1.getName());
    assertEquals("test1@example.com", u1.getEmail());

    final UserEntity u2 = map.get(2L);
    assumeTrue(u2 != null);
    assertEquals(2L, u2.getId());
    assertEquals("ユーザー2", u2.getName());
    assertEquals("test2@example.com", u2.getEmail());

    final UserEntity u3 = map.get(3L);
    assumeTrue(u3 != null);
    assertEquals(3L, u3.getId());
}

private Map<Long, UserEntity> getUsers() {
    return SampleCodesTest.mapOf(
                kv(1L, new UserEntity(1L, "ユーザー1", "test1@example.com", "password1")),// 
                kv(3L, new UserEntity(3L, "ユーザー3", "test3@example.com", "password3"))//
        );
}

なるべく一つのテストメソッドには一つのアサーションにするべきなのはわかってはいるけれど、どうしても各プロパティの値を調べたいし、 equals を実装して簡単にアサーションしたいけど何らかの制約により equals が実装できないので仕方なくプロパティの個数だけアサーションをするようなケースです。

この複数回アサーションを実行するのがまずいのは、アサーションが失敗するケースがいくつかある場合に、最初の一つだけが落ちて、残りのアサーションが実行されないので、何が落ちているのかがわからないということにあります。実際上に書いたテストも途中でabortされていて、何が通って何が落ちているのかがわかりません。

そこでJUnit5では、複数のアサーションを引数にとって、すべてのアサーションを実行するアサーション Assertions.assertAll が定義されています。それでは、上記のテストを assertAll を用いて書き換えてみます。

@Test
void secondTest() {
    final Map<Long, UserEntity> map = getUsers();

    final UserEntity u1 = map.get(1L);
    final UserEntity u2 = map.get(2L);
    final UserEntity u3 = map.get(3L);

    assertAll(
            () -> assumeTrue(u1 != null),
            () -> assertEquals(1L, u1.getId()),
            () -> assertEquals("ユーザー1", u1.getName()),
            () -> assertEquals("test1@example.com", u1.getEmail()),
            () -> assumeTrue(u2 != null),
            () -> assertEquals(2L, u2.getId()),
            () -> assertEquals("ユーザー2", u2.getName()),
            () -> assertEquals("test2@example.com", u2.getEmail()),
            () -> assumeTrue(u3 != null),
            () -> assertEquals(3L, u3.getId())
    );
}

さて、これでもう安心と言えるかというと、実はそんなことはありません。なぜかというと、 assertAll の中にテストをabortするものが入っていると、すべてを実行してくれなくなるためです。 assertAll は一つでも落ちるものがある場合にはアサーションが失敗になるように複数のアサーションをまとめてくれる機能ですが、一つでもabortがあると、途中で止まってしまい、現在のテストの状態がわからなくなります。上記のテストも途中でabortされてしまい、最初のパターンと何ら変わるところがありません。

そこで、用いるのが、これまたJUnit5の標準に入っている DynamicTest@TestFactory です。

DynamicTest@TestFactory は動的にテストを作ると説明されていることがありますが、複数のアサーションをまとめたい場合に適用するのが、最適な活用パターンではないかと思います。

では、先ほどのテストを書き換えてみましょう。

@TestFactory
List<DynamicTest> thirdTest() {
    final Map<Long, UserEntity> map = getUsers();
    final List<DynamicTest> tests = new ArrayList<>();

    final UserEntity u1 = map.get(1L);
    tests.add(dynamicTest("ユーザー1",//
            () -> assertAll(
                    () -> assumeTrue(u1 != null),
                    () -> assertEquals(1L, u1.getId()),
                    () -> assertEquals("ユーザー1", u1.getName()),
                    () -> assertEquals("test1@example.com", u1.getEmail())
            )));

    final UserEntity u2 = map.get(2L);
    tests.add(dynamicTest("ユーザー2",//
            () -> assertAll(
                    () -> assumeTrue(u2 != null),
                    () -> assertEquals(2L, u2.getId()),
                    () -> assertEquals("ユーザー2", u2.getName()),
                    () -> assertEquals("test2@example.com", u2.getEmail())
            )));

    final UserEntity u3 = map.get(3L);
    tests.add(dynamicTest("ユーザー3",//
            () -> assertAll(
                    () -> assumeTrue(u3 != null),
                    () -> assertEquals(3L, u3.getId())
            )));

    return tests;
}

このテストを実行すると、「ユーザー1」と「ユーザー3」というテストが通っていて、「ユーザー2」というテストがabortされている状態がわかります。これで現在のテストの状態がわかるようになりました。


という感じで、何らかの理由により複数のアサーションをせざるを得ない時に、JUnit5の機能を用いてテストを整理するとテストの状態がよくわかるようになるという話でした。

ジェネリクス勉強会に行ってきた件

表題のとおりです。

connpass.com

おっさんなのにわりと知らないことも多くて、不勉強を実感しました。

HaskellGHC.Genericsはあまりよくわかってないので、ちゃんとやるべきですね…

なお、当日のツイートは #ジェネリクス勉強会 since:2017-06-24 until:2017-06-25 で検索すれば見つかると思います。


で、 @kmizu さんがした、抽象型でジェネリクスの主要な概念を実装するという 発表内容 が面白く、高階型もエミュレートできるとのことでしたので、家に帰ってから発表されてた内容を思い出しつつ自分でも書いてみました。

題材としては Functor(二番煎じ) + Maybe Functor(実装が簡単)を用いています。


まず関数を定義します。

trait Fun1 {
  type I
  type O

  def apply(i: I): O
}

次に高階型を定義します。(発表中に気づいて、「なるほど」と思った)

trait Hk1 {
  type E
}

次に Functor を定義します

trait Functor {self =>
  type F <: Hk1
  type A
  type B

  //noinspection ApparentRefinementOfResultType
  def map(o: F {type E = self.A}, f: Fun1 {type I = self.A; type O = self.B}): F { type E = self.B }

  def map(f: Fun1 {type I = self.A; type O = self.B}): Fun1 {
    type I = F { type E = self.A }
    type O = F { type E = self.B }
  } = new Fun1 {
    override type I = F { type E = self.A }
    override type O = F { type E = self.B }

    override def apply(input: F { type E = self.A }): F { type E = self.B } = self.map(input, f) 
  }  
}

ちなみに僕は初めて trait Foo {self => と書いたときの self が何を指しているのかがわかりました

@kmizuさんの発表では最初の map 関数だけ定義されていましたが、Haskellfmap f を新たに関数として用いているパターンを見かけるので、 map(f: A => B): F[A] => F[B] となるような関数も定義してみました(完全に自己満足です)。

で、 Maybe 型を定義します。

trait Maybe extends Hk1 {
  def getValue: E
}

とりあえず、具体的に IntMaybe を定義します

object NothingInt extends Maybe {
  override type E = Int
  override def getValue: Int = throw new NoSuchElementException("this is Nothing")
}

case class JustInt(value: Int) extends Maybe {
  override type E = Int
  override def getValue: Int = value
}

同様に StringMaybe も定義します

object NothingString extends Maybe {
  override type E = String
  override def getValue: String = throw new NoSuchElementException("this is Nothing")
}

case class JustString(value: String) extends Maybe {
  override type E = String
  override def getValue: String = value
}

ジェネリクスを使えば Just[A]Nothing を定義すれば済みますが、型1つずつに型をつくらないといけない辺りが実用性が…と感じられますが、コンピューターが自動でこのようなボイラープレートなコードを生成するのであれば気にならないと思います(コンパイル時に型を決定してこれと同様なバイナリを生成するなどの作戦はあるかと思う)。

そして Functorインスタンスを作ります

object MaybeIntToStringFunctor extends Functor {
  override type F = Maybe
  override type A = Int
  override type B = String

  override def map(o: Maybe { type E = Int }, f: Fun1 { type I = Int ;type O = String }): Maybe { type E = String } =
    o match {
      case NothingInt => NothingString
      case JustInt(v) => JustString(f(v))
    }
}

では、実際にこれを使ってみたいと思います。

// Fun1 を作るのが面倒なので、通常の関数から Fun1 を作る関数を作る
def fun[A,B](f: A => B): Fun1 { type I = A; type O = B } = new Fun1 { type I = A; type O = B; override def apply(i: I): O = f(i) }

// とりあえず、 Functor のインスタンスを implicit な変数にあれする
implicit val maybeIntToStringFunctor = MaybeIntToStringFunctor

// map という関数を作って Functor の map 関数を呼べるようにする
def map(f: Fun1{ type I = Int; type O = String})(implicit tci: Functor { type A = Int; type B = String }): Fun1{ type I = tci.F { type E = Int }; type O = tci.F { type E = String } } = new Fun1 {
  type I = tci.F { type E = Int }
  type O = tci.F { type E = String }
  override def apply(i: I): O = tci.map(i,f)
}

// map関数にFun1を適用して Maybe をmappingする関数を作る
val intToStringMapper = map(fun(i => "the value is " + i))

// ↑の関数に JustInt(10) を適用する
intToStringMapper(JustInt(10)) // -> JustString("the value is 10")

// ↑の関数にNothingInt を適用する
intToStringMapper(NothingInt) // -> NothingString

f:id:mike_neck:20170625010223p:plain

という感じでScalaよくわかってませんが作れました


この Functor ですが、 String から Int にマップする場合にはまた別の Functorインスタンスを作る必要があり、この形で人間がコードを書くのは若干辛いものがありました…(本当はモッを作ろうとしたけど、やめた)