TypeScript環境でnycとtapeを使って、カバレッジを取るテスト環境を整える。
初めはavaを使おうと思ってたんです。
下記の説明を見たところコンパイルしてからやってね、みたいな形になっています。 github.com
avaはmagic-assertを使っており、jsに対してassertの表示を見やすくするための処理が入ってるはずです。 そのため、babel等を使う前提で作られています。そのため、TypeScriptへの対応はまだされていない模様です。
というわけで諦めて、tapeとts-nodeとnycでカバレッジを取ってみます。
yarn add tape ts-node nyc typescript @types/tape -D
package.jsonにnpm scriptsを記述します。 今回はコンソールに出力したいのとhtmlでも出力してみたいので reporterを2つ指定してあります。
{ .... "scripts":{ "test": "nyc --reporter=html --reporter=text tape src/test/front/**.ts" } }
同じく、package.jsonにnycの設定を追加します。
{ .... "nyc":{ "include": [ "src/main/front/**/*.ts" ], "extension": [ ".ts" ], "require": [ "ts-node/register" ], "all": true } }
サンプルコードとテストコードです。 tapeのendメソッドを呼ばなくても動きそうだと思ったのですがうまくいかず・・・
// module.ts export default function(){ console.log("test") if(typeof window=="object"){ console.log("not reached") } } // module.spec.ts import * as test from "tape" import module from "../../main/front/module" test('xxx', (t)=>{ module() t.isEqual("a","b") t.end() })
npm test
こんなログが出ます。
PS D:\workspace\spring-sandbox\oauth> npm test > spring-sandbox@1.0.0 test D:\workspace\spring-sandbox\oauth > nyc --reporter=html --reporter=text tape src/test/front/**.ts TAP version 13 # xxx test not ok 1 should be equal --- operator: equal expected: 'b' actual: 'a' at: Test.test (D:\workspace\spring-sandbox\oauth\src\test\front\test.ts:5:5) ... 1..1 # tests 1 # pass 0 # fail 1 ----------|----------|----------|----------|----------|----------------| File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines | ----------|----------|----------|----------|----------|----------------| All files | 75 | 50 | 100 | 75 | | test.ts | 75 | 50 | 100 | 75 | 5 | ----------|----------|----------|----------|----------|----------------| npm ERR! Test failed. See above for more details.
htmlのカバレッジレポートもこんな感じで出力されました。
まとめ
avaが使いたかったところから迷走した感があります。
よくよく考えたらTypeScript使うので jasmineでいい気がした。(apiがわからない問題は解決するはず) つまり、カバレッジも取れそうだし jasmineベースのjestでいい気がしました。
また、環境構築し直しです。
アノテーションを体が欲している:NotNull:NotNull:NotNull
みんなだいすきアノテーション みんな大嫌いアノテーション あの手この手でアノテーション
というわけでNotNullアノテーションが付けられたメソッドを バイトコード操作する話です。
NotNull制約をアノテーションだけで実現します。
似たような仕組みはChecker Frameworkだったり Bean Validationだったり、Apache PolygeneのConstraintだったり あるわけでして。
まぁそんな仕組みを書いてみたかった、という話になります。
今回の話は紆余曲折します。
アノテーションの書ける場所の話
現在、Java8では色んな箇所にAnnotationを記述することができます。
JVM Specificationには以下のサンプルが途中都合上出てきます。 「Outer.Middle.@Foo Inner」なんて、普段使わない気もするのですが 面白いですね。
一番上のStringの二次元の配列の例はどこにAnnotation書いてるのか分かりにくい・・・
@Foo String[][] String @Foo [][] String[] @Foo [] @Foo Outer.Middle.Inner Outer.@Foo Middle.Inner Outer.Middle.@Foo Inner @Foo Map<String,Object> Map<@Foo String,Object> Map<String,@Foo Object> List<@Foo ? extends String> List<? extends @Foo String>
さて、そんなこんなでJava8ではメソッド(コンストラクタ)の引数に自分の型を書くことができます。 レシーバパラメータと呼ばれる機能のようです。 JavaSE8リリース記念!マイナーな言語仕様を紹介してみる(交差型キャスト,レシーバパラメータ(仮引数にthis)) - きつねとJava!
使い方については、以下のようにアノテーションをつけることを想定されたような機能です。 Checker Frameworkのようにアノテーションで型検査をするためにつけるとかでしょうね。
class Test{ void m(Testt his, int n1, int n2){} } class Test{ void m(@Sample Test this, int n1, int n2){} }
TypeScriptも似たような機能があって、 こちらはJavaScriptらしさがあります。 Playground · TypeScript
ECMAScriptのほうにも似たような話が上がってるみたいです。(このリポジトリbind-operatorなんですけどね) Explicit naming of `this` · Issue #30 · tc39/proposal-bind-operator · GitHub
こっちはthisに別名をつけられるようにしません?みたいな話で JavaScriptではthisがコロコロ変わることがあるので、下のコードみたいに一時変数への代入をしたりします。
function join(){ const array = this; return array.join(", "); }
これに合わせてflow typeのtype annotationで型検査できるとか楽しそうですね。 (現状、似たような機能ってあるんですかね?なんかある気もするけどぱっと見、見つからなかった)
型アノテーションにthisを使える、みたいなのはあるようですが。 Flow | Classes
NotNullアノテーションがあるメソッドに対してnullのチェックを追加する
これはお題目通りのコードです。
今回はバイトコード操作で書いてみます。 方法としてはメソッドのインターセプトとかでも書けそうですね。 InjectionPointからMethodが取れるのでそこからgetParameterAnnotations()でアノテーションが取れるので・・・ ってそっちからの方が良かったのでは・・・
下のようなサンプルのコードを用意しました。
public class Example { public static void staticMethod(@NotNull String notNullString) {} public void method(@NotNull String notNullString) {} public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {} }
では、これをこんな感じにしましょう。
public class Example { public static void staticMethod(@NotNull String notNullString) { Objects.requireNonNull(notNullString, "notNullString is required"); } public void method(@NotNull String notNullString) { Objects.requireNonNull(notNullString, "notNullString is required"); } public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) { Objects.requireNonNull(this, "this is required"); Objects.requireNonNull(notNullString, "notNullString is required"); } }
できませんでした。 こうなります。
public class Example { public static void staticMethod(@NotNull String notNullString) { Objects.requireNonNull(notNullString, "notNullString is required"); } public void method(@NotNull String notNullString) { Objects.requireNonNull(notNullString, "notNullString is required"); } public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) { Objects.requireNonNull(notNullString, "notNullString is required"); } }
気を取り直して実際書いたコードを下に示します。 javaagentで利用する想定で書いたのでClassFileTransformerを実装しています。
public class NotNullInstrumentation implements ClassFileTransformer { public ClassPool pool; public NotNullInstrumentation(ClassPool pool) { this.pool = pool; } @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try (ByteArrayInputStream stream = new ByteArrayInputStream(classfileBuffer)) { CtClass clazz = pool.makeClass(stream); for (CtMethod method : clazz.getDeclaredMethods()) { MethodInfo info = method.getMethodInfo(); CodeAttribute codeAttribute = info.getCodeAttribute(); LocalVariableAttribute attribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag); Object[][] annotationArray = method.getParameterAnnotations(); for (int i = 0; i < annotationArray.length; i++) { Object[] annotations = annotationArray[i]; String name = attribute.variableName(i + (Modifier.isStatic(method.getModifiers()) ? 0 : 1)); for (Object annotation : annotations) { if (annotation instanceof NotNull) { method.insertBefore("java.util.Objects.requireNonNull($" + (i + 1) + ",\"" + name + " is required\");"); } } } } return clazz.toBytecode(); } catch (IOException | ClassNotFoundException | CannotCompileException e) { throw new RuntimeException(e); } } }
宣言されたメソッドとそのメソッドの引数のアノテーションを見て NotNullアノテーションを見てメソッドの先頭に処理を追加しています。
今回実装した、nullを検査するコードは実は不完全でもう少し注意深くコードを書かないといけません。 クラスパスにNotNullアノテーションが入っていないとエラーになってしまいます。 やるとしたら、完全修飾クラス名を取得して比較とかするんでしょうね。
横道:ローカル変数名の取得
LovalVariableAttributeはローカル変数テーブルを表しています。 なぜここからデータを取っているかというと Javacはオプションを渡さない限り、メソッドの引数名を残しません。 このクラスのgetNameでは、コンパイルオプションを渡してない限り、変数名は帰ってきません。(argNになります) Parameter (Java Platform SE 8 )
テストコード
このコードをテストします。といってもエラーがでないことを確認するだけです。 今回のお供はjMockitさんです。
@Test public void test() throws Exception { new MockUp(Class.forName("javassist.CtClassType")) { @Mock public boolean isFrozen() { return false; } }; NotNullInstrumentation inst = new NotNullInstrumentation(ClassPool.getDefault()); inst.transform(getClass().getClassLoader(), forInstruments(Example.class), null, null, getByteCode(Example.class)); }
なぜisFrozenにmockupをしているかというと 実行中にClassの書き換えを行うのでjavassistから例外がスローされるからです。
(そういえば、jacocoとjmockit入れた時に気づいたのですが、jmockitはVM実行中にjavaagentを与えているようですね。何やらイケない香りがしますね。)
で、先ほどのコードでなぜthisのrequireNonNullが出力できないのかという話をします。 その前にjavapします。
Annotationさんや~どこぞ~
ちょっとテストのためにメソッドを追加した以下のクラスをjavapしてみます。
public class Example { public static void staticMethod(@NotNull String notNullString) {} public void method(@NotNull String notNullString) {} public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {} @NotNull public void methodAnnotated(@NotNull Example this,@NotNull String notNullString) {} public List<@NotNull String> typeAnnotated() { return null; } }
下のような情報が出てきます。
~~~~ public static void staticMethod(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC RuntimeVisibleParameterAnnotations: parameter 0: 0: #17() Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 notNullString Ljava/lang/String; RuntimeVisibleTypeAnnotations: 0: #17(): METHOD_FORMAL_PARAMETER, param_index=0 public void method(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC RuntimeVisibleParameterAnnotations: parameter 0: 0: #17() Code: stack=0, locals=2, args_size=2 0: return LineNumberTable: line 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lcom/github/wreulicke/bean/validation/Example; 0 1 1 notNullString Ljava/lang/String; RuntimeVisibleTypeAnnotations: 0: #17(): METHOD_FORMAL_PARAMETER, param_index=0 public void thisAnnotated(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC RuntimeVisibleParameterAnnotations: parameter 0: 0: #17() Code: stack=0, locals=2, args_size=2 0: return LineNumberTable: line 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lcom/github/wreulicke/bean/validation/Example; 0 1 1 notNullString Ljava/lang/String; RuntimeVisibleTypeAnnotations: 0: #17(): METHOD_FORMAL_PARAMETER, param_index=0 1: #17(): METHOD_RECEIVER public void methodAnnotated(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC RuntimeVisibleAnnotations: 0: #17() RuntimeVisibleParameterAnnotations: parameter 0: 0: #17() Code: stack=0, locals=2, args_size=2 0: return LineNumberTable: line 15: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lcom/github/wreulicke/bean/validation/Example; 0 1 1 notNullString Ljava/lang/String; RuntimeVisibleTypeAnnotations: 0: #17(): METHOD_FORMAL_PARAMETER, param_index=0 1: #17(): METHOD_RECEIVER public java.util.List<java.lang.String> typeAnnotated(); descriptor: ()Ljava/util/List; flags: ACC_PUBLIC Signature: #28 // ()Ljava/util/List<Ljava/lang/String;>; Code: stack=1, locals=1, args_size=1 0: aconst_null 1: areturn LineNumberTable: line 18: 0 LocalVariableTable: Start Length Slot Name Signature 0 2 0 this Lcom/github/wreulicke/bean/validation/Example; RuntimeVisibleTypeAnnotations: 0: #17(): METHOD_RETURN, location=[TYPE_ARGUMENT(0)] }
おやおや色々情報が出てきました。
今回見たいのはレシーバパラメータのメソッドなので少し絞ってみます。
public void thisAnnotated(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC RuntimeVisibleParameterAnnotations: parameter 0: 0: #17() Code: stack=0, locals=2, args_size=2 0: return LineNumberTable: line 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lcom/github/wreulicke/bean/validation/Example; 0 1 1 notNullString Ljava/lang/String; RuntimeVisibleTypeAnnotations: 0: #17(): METHOD_FORMAL_PARAMETER, param_index=0 1: #17(): METHOD_RECEIVER
RuntimeVisibleParameterAnnotationsには情報が一つついていますね。(あれ?レシーバパラメータのあのテーションは?) また、先ほどお話したLocalVariableTableには変数名が残っています。 先ほどのコードではここからデータを取り出した形になっています。
17の情報はConstantPoolに保存されています。
おや、#17にはLjavax/validation/constraints/NotNull;ですね。 情報をこんな感じでテーブルにして保持しているのですねー。
Constant pool: #1 = Class #2 // com/github/wreulicke/bean/validation/Example #2 = Utf8 com/github/wreulicke/bean/validation/Example #3 = Class #4 // java/lang/Object #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Methodref #3.#9 // java/lang/Object."<init>":()V #9 = NameAndType #5:#6 // "<init>":()V #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/github/wreulicke/bean/validation/Example; #14 = Utf8 staticMethod #15 = Utf8 (Ljava/lang/String;)V #16 = Utf8 RuntimeVisibleParameterAnnotations #17 = Utf8 Ljavax/validation/constraints/NotNull; #18 = Utf8 notNullString #19 = Utf8 Ljava/lang/String; #20 = Utf8 RuntimeVisibleTypeAnnotations #21 = Utf8 method #22 = Utf8 thisAnnotated #23 = Utf8 methodAnnotated #24 = Utf8 RuntimeVisibleAnnotations #25 = Utf8 typeAnnotated #26 = Utf8 ()Ljava/util/List; #27 = Utf8 Signature #28 = Utf8 ()Ljava/util/List<Ljava/lang/String;>; #29 = Utf8 SourceFile #30 = Utf8 Example.java {
で、ですね結局、レシーバパラメータの情報が引数のアノテーションと一緒に取れないのは 入ってるところが別だから、ですね。
引数のアノテーションはRuntime(In)VisibleParameterAnnotations アノテーション全般はRuntime(In)VisibleTypeAnnotationsに入っています。
引数のアノテーションはRuntime(In)VisibleParameterAnnotationsのアノテーションを取得しているからですね~ ローカル変数のアノテーションもこちらに入っているようです。
で、ですね。一応、JavassistにもRuntimeVisibleTypeAnnotationsに対応する TypeAnnotationsAttributeが入っています。
が、現状のJavassistのAPIではアノテーションの数しか取れません。 また、アノテーションの配置場所とアノテーションが一緒に取れないといけません。 なかなか骨が折れそうです。 (ローカル変数や型パラメータに対するアノテーションの情報もここに入るので)
asmなら少し調べたところ、触れそうですね。 MethodVisitor (ASM 5.1 Documentation)
visitTypeAnnotationとvisitLocalVariableAnnotationというAPIが存在します。
まとめ
JavapしてJVM Specificationとにらめっこした話、でした。
ASM触ればいい気がしてきた。
みんなもっとアノテーション塗れになろうや!!!!!
備忘録としてここに書いておきます。
JavaFX入門した
やる気ない感じでJavaFXを軽く触ることにした。
ごらんの有様だよ!!
ドラッグ・アンド・ドロップしたファイル(.jar or .class)をデコンパイルします。
ドラッグ・アンド・ドロップしたファイルと同じディレクトリにdecompiledというフォルダを作成し
そこにデコンパイルされたソースを展開します。
一部デコンパイルできないケースがあると思いますが
ライブラリが対応してないので許してください。。。
というわけでJavaFXでデコンパイルするだけのアプリケーションを作ったよ、という話でした。
リポジトリはこちらになります。
GitHub - Wreulicke/decompiler-javafx-application: JavaFX Application for decompile tool using windup-procyon