From 2e191909018986e35bceedb8339dd0b10c4d63da Mon Sep 17 00:00:00 2001 From: tateisu Date: Thu, 28 Oct 2021 08:37:39 +0900 Subject: [PATCH] =?UTF-8?q?kotlin=20=E3=82=921.5.30=20=E3=81=AB=E6=88=BB?= =?UTF-8?q?=E3=81=99=E3=80=82androidTest=E3=81=A8test=E3=82=92=E9=80=9A?= =?UTF-8?q?=E3=81=99=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apng_android/build.gradle | 4 +- app/build.gradle | 33 +- .../subwaytooter/ExampleInstrumentedTest.java | 23 - .../subwaytooter/ExampleInstrumentedTest.kt | 18 + .../juggler/subwaytooter/JsonArrayForEach.kt | 96 ++- .../subwaytooter/TestMisskeyMentionAndroid.kt | 170 ++--- .../juggler/subwaytooter/TestTootInstance.kt | 2 +- .../juggler/subwaytooter/WordTrieTreeTest.kt | 331 +++++----- .../subwaytooter/api/TestDuplicateMap.kt | 424 +++++++------ .../subwaytooter/api/TestTootApiClient.kt | 587 ++++++++++-------- .../api/entity/TestEntityUtils.kt | 509 ++++++++------- .../api/entity/TestTootAccount.kt | 110 ++-- .../database/TestArrayListSizeBug.kt | 26 + .../subwaytooter/database/TestDatabase.kt | 66 ++ .../subwaytooter/util/TestBucketList.kt | 29 +- .../subwaytooter/util/TestHtmlDecoder.kt | 75 +-- .../main/java/jp/juggler/subwaytooter/App1.kt | 4 +- .../main/java/jp/juggler/subwaytooter/Pref.kt | 6 +- .../juggler/subwaytooter/table/AcctColor.kt | 2 +- .../jp/juggler/subwaytooter/table/AcctSet.kt | 2 +- .../juggler/subwaytooter/table/ClientInfo.kt | 2 +- .../subwaytooter/table/ContentWarning.kt | 2 +- .../jp/juggler/subwaytooter/table/FavMute.kt | 2 +- .../subwaytooter/table/HighlightWord.kt | 2 +- .../jp/juggler/subwaytooter/table/LogData.kt | 2 +- .../juggler/subwaytooter/table/MediaShown.kt | 2 +- .../jp/juggler/subwaytooter/table/MutedApp.kt | 2 +- .../juggler/subwaytooter/table/MutedWord.kt | 2 +- .../subwaytooter/table/NotificationCache.kt | 2 +- .../table/NotificationTracking.kt | 26 +- .../juggler/subwaytooter/table/PostDraft.kt | 2 +- .../subwaytooter/table/SavedAccount.kt | 106 +++- .../table/SubscriptionServerKey.kt | 2 +- .../jp/juggler/subwaytooter/table/TagSet.kt | 6 +- .../subwaytooter/table/UserRelation.kt | 11 +- .../subwaytooter/util/CustomEmojiCache.kt | 10 +- .../main/java/jp/juggler/util/ColumnMeta.kt | 7 + .../subwaytooter/TestArrayListSizeBug.kt | 23 + build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 10 +- gradlew | 298 +++++---- gradlew.bat | 53 +- 43 files changed, 1724 insertions(+), 1367 deletions(-) delete mode 100644 app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java create mode 100644 app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.kt create mode 100644 app/src/androidTest/java/jp/juggler/subwaytooter/database/TestArrayListSizeBug.kt create mode 100644 app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt create mode 100644 app/src/test/java/jp/juggler/subwaytooter/TestArrayListSizeBug.kt diff --git a/apng_android/build.gradle b/apng_android/build.gradle index c2029027..aa356b40 100644 --- a/apng_android/build.gradle +++ b/apng_android/build.gradle @@ -34,8 +34,8 @@ repositories { dependencies { testImplementation "junit:junit:$junit_version" - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' api project(':apng') // 'api' に指定した依存関係はこのライブラリの利用者に公開されます diff --git a/app/build.gradle b/app/build.gradle index 0b2dd30d..df2417dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,7 +41,8 @@ android { ] } buildFeatures { - compose true + // 諸事情でまだcomposeを導入できない。詳細は依存関係の項に記載 + // compose true } composeOptions { kotlinCompilerExtensionVersion compose_version @@ -115,6 +116,10 @@ dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.aar'], dir: 'src/main/libs') + // targetSdkVersion 31 で androidTest 時に android:exported 云々で怒られる問題の対策 + // https://github.com/android/android-test/issues/1022 + androidTestImplementation "androidx.test:core:1.4.0" + androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0-alpha4', { exclude group: 'com.android.support', module: 'support-annotations' }) @@ -152,7 +157,6 @@ dependencies { // https://firebase.google.com/support/release-notes/android implementation "com.google.firebase:firebase-messaging:22.0.0" - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" @@ -176,7 +180,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion" testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" - androidTestImplementation "'com.squareup.okhttp3:mockwebserver:$okhttpVersion" + androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" def glideVersion = '4.12.0' implementation "com.github.bumptech.glide:glide:$glideVersion" @@ -241,18 +245,19 @@ dependencies { // optional - Test helpers for LiveData testImplementation "androidx.arch.core:core-testing:$arch_version" - implementation "com.google.accompanist:accompanist-flowlayout:0.20.0" - - implementation "androidx.compose.ui:ui:$compose_version" - implementation "androidx.compose.material:material:$compose_version" - implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation "androidx.compose.runtime:runtime-livedata:$compose_version" - implementation "androidx.compose.material:material-icons-extended:$compose_version" - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" - debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" - - implementation 'androidx.activity:activity-compose:1.4.0-rc01' + // composeはkotlin 1.5.31を要求するが + // https://gist.github.com/tateisu/cbda451135d2b5c9b69f7ac4599f9833 の問題があるので1.5.31を使えない + // 治るまでcompose導入を諦める +// implementation "androidx.compose.ui:ui:$compose_version" +// implementation "androidx.compose.material:material:$compose_version" +// implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" +// implementation "androidx.compose.runtime:runtime-livedata:$compose_version" +// implementation "androidx.compose.material:material-icons-extended:$compose_version" +// androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" +// debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" +// implementation 'androidx.activity:activity-compose:1.4.0-rc01' +// implementation "com.google.accompanist:accompanist-flowlayout:0.20.0" } diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java b/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java deleted file mode 100644 index c615292e..00000000 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package jp.juggler.subwaytooter; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -// Android instrumentation test は run configuration を編集しないと Empty tests とかいうエラーになります - -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext(){ - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals( "jp.juggler.subwaytooter", appContext.getPackageName() ); - } -} diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..2116bfd5 --- /dev/null +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.kt @@ -0,0 +1,18 @@ +package jp.juggler.subwaytooter + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +// Android instrumentation test は run configuration を編集しないと Empty tests とかいうエラーになります +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Assert.assertEquals("jp.juggler.subwaytooter", appContext.packageName) + } +} diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/JsonArrayForEach.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/JsonArrayForEach.kt index 7534f1d7..5f7ded69 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/JsonArrayForEach.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/JsonArrayForEach.kt @@ -1,14 +1,12 @@ package jp.juggler.subwaytooter import androidx.test.runner.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import jp.juggler.util.* +import jp.juggler.util.jsonArray import org.jetbrains.anko.collections.forEachReversedByIndex import org.jetbrains.anko.collections.forEachReversedWithIndex import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith /** * Example local unit test, which will execute on the development machine (host). @@ -17,48 +15,48 @@ import org.junit.Assert.assertEquals */ @RunWith(AndroidJUnit4::class) class JsonArrayForEach { - - @Test - @Throws(Exception::class) - fun test(){ - val array = jsonArray{ - add("a") - add("b") - add( null) - add( null) - } - - var count = 0 - - array.forEach { - println("JsonArray.forEach $it") - ++count - } - - array.forEachIndexed { i,v-> - println("JsonArray.forEachIndexed $i $v") - ++count - } - - array.forEachReversedByIndex { - println("JsonArray.downForEach $it") - ++count - } - - array.forEachReversedWithIndex{ i,v-> - println("JsonArray.downForEachIndexed $i $v") - ++count - } - - for( o in array.iterator() ){ - println("JsonArray.iterator $o") - ++count - } - for( o in array.asReversed().iterator() ){ - println("JsonArray.reverseIterator $o") - ++count - } - - assertEquals(count,24) - } + + @Test + @Throws(Exception::class) + fun test() { + val array = jsonArray { + add("a") + add("b") + add(null) + add(null) + } + + var count = 0 + + array.forEach { + println("JsonArray.forEach $it") + ++count + } + + array.forEachIndexed { i, v -> + println("JsonArray.forEachIndexed $i $v") + ++count + } + + array.forEachReversedByIndex { + println("JsonArray.downForEach $it") + ++count + } + + array.forEachReversedWithIndex { i, v -> + println("JsonArray.downForEachIndexed $i $v") + ++count + } + + for (o in array.iterator()) { + println("JsonArray.iterator $o") + ++count + } + for (o in array.asReversed().iterator()) { + println("JsonArray.reverseIterator $o") + ++count + } + + assertEquals(count, 24) + } } \ No newline at end of file diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt index 1e623ebf..a650ca54 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt @@ -10,91 +10,91 @@ import org.junit.runner.RunWith // Android instrumentation test は run configuration を編集しないと Empty tests とかいうエラーになります @RunWith(AndroidJUnit4::class) class TestMisskeyMentionAndroid { - - @Test - fun testBracket() { - // [] 空の文字セットはパースエラーになる。 - // val re1="""[]""".toRegex() // error 空の文字クラス - - // [[] や [[]] はパースエラーになる。 - // val re1="""[[]""".toRegex() // error 閉じ括弧が足りない - // val re1="""[[]]""".toRegex() // error 内側が空の文字クラス - - // 最低でも1文字を含む。 - assertEquals(true, """[]]""".toRegex().matches("]")) - - // 1文字あけた次からは閉じ括弧として扱われる。 - assertEquals(true, """[ ]]""".toRegex().matches(" ]")) - - // 閉じ括弧が単体で出たら文字クラスにならない。 - assertEquals(true, """]""".toRegex().matches("]")) - - // 閉じ括弧が足りないのはエラーになる。 - // val a="""[[ ]""".toRegex() - // IDEで警告が出るが、Androidは正規表現エンジンが異なるので仕方ない - @Suppress("RegExpRedundantNestedCharacterClass") - assertEquals(true, """[[ ]]][ ]""".toRegex().matches(" ] ")) + @Test + fun testBracket() { + // [] 空の文字セットはパースエラーになる。 + // val re1="""[]""".toRegex() // error 空の文字クラス + + // [[] や [[]] はパースエラーになる。 + // val re1="""[[]""".toRegex() // error 閉じ括弧が足りない + // val re1="""[[]]""".toRegex() // error 内側が空の文字クラス + + // 最低でも1文字を含む。 + assertEquals(true, """[]]""".toRegex().matches("]")) + + // 1文字あけた次からは閉じ括弧として扱われる。 + assertEquals(true, """[ ]]""".toRegex().matches(" ]")) + + // 閉じ括弧が単体で出たら文字クラスにならない。 + assertEquals(true, """]""".toRegex().matches("]")) + + // 閉じ括弧が足りないのはエラーになる。 + // val a="""[[ ]""".toRegex() + + // IDEで警告が出るが、Androidは正規表現エンジンが異なるので仕方ない + @Suppress("RegExpRedundantNestedCharacterClass") + assertEquals(true, """[[ ]]][ ]""".toRegex().matches(" ] ")) + + } + + @Test + @Throws(Exception::class) + fun testAsciiPattern() { + // \w \d \W \D 以外の文字は素通しする + assertEquals("""ab\c\\""", """ab\c\\""".asciiPatternString()) + assertEquals("""[A-Za-z0-9_]""", """\w""".asciiPatternString()) + assertEquals("""[A-Za-z0-9_-]""", """[\w-]""".asciiPatternString()) + assertEquals("""[^A-Za-z0-9_]""", """\W""".asciiPatternString()) + assertEquals("""[0-9]""", """\d""".asciiPatternString()) + assertEquals("""[0-9:-]""", """[\d:-]""".asciiPatternString()) + assertEquals("""[^0-9]""", """\D""".asciiPatternString()) + + // 文字セットの中の \W \D は変換できないので素通しする + assertEquals("""[\W]""", """[\W]""".asciiPatternString()) + assertEquals("""[\D]""", """[\D]""".asciiPatternString()) + + // エスケープ文字の後に何もない場合も素通しする + assertEquals("""\""", """\""".asciiPatternString()) + + } + + @Test + fun testMisskeyMention() { + fun findMention(str: String): String? { + val m = TootAccount.reMisskeyMentionMFM.matcher(str) + return if (m.find()) m.group(0) else null + } + assertEquals(null, findMention("")) + assertEquals(null, findMention("tateisu")) + assertEquals("@tateisu", findMention("@tateisu")) + assertEquals("@tateisu", findMention("@tateisuほげ")) + assertEquals("@tateisu@mastodon.juggler.jp", findMention("@tateisu@mastodon.juggler.jp")) + assertEquals("@tateisu@mastodon.juggler.jp", findMention("@tateisu@mastodon.juggler.jpほげ")) + assertEquals("@tateisu", findMention("@tateisu@マストドン3.juggler.jp")) + assertEquals( + "@tateisu@xn--3-pfuzbe6htf.juggler.jp", + findMention("@tateisu@xn--3-pfuzbe6htf.juggler.jp") + ) + } + + @Test + fun testMastodonMention() { + fun findMention(str: String): String? { + val m = TootAccount.reCountMention.matcher(str) + return if (m.find()) m.group(0) else null + } + assertEquals(null, findMention("")) + assertEquals(null, findMention("tateisu")) + assertEquals("@tateisu", findMention("@tateisu")) + assertEquals("@tateisu", findMention("@tateisuほげ")) + assertEquals("@tateisu@mastodon.juggler.jp", findMention("@tateisu@mastodon.juggler.jp")) + assertEquals("@tateisu@mastodon.juggler.jp", findMention("@tateisu@mastodon.juggler.jpほげ")) + assertEquals("@tateisu@マストドン3.juggler.jp", findMention("@tateisu@マストドン3.juggler.jp")) + assertEquals( + "@tateisu@xn--3-pfuzbe6htf.juggler.jp", + findMention("@tateisu@xn--3-pfuzbe6htf.juggler.jp") + ) + } - } - - @Test - @Throws(Exception::class) - fun testAsciiPattern() { - // \w \d \W \D 以外の文字は素通しする - assertEquals("""ab\c\\""", """ab\c\\""".asciiPatternString()) - assertEquals("""[A-Za-z0-9_]""", """\w""".asciiPatternString()) - assertEquals("""[A-Za-z0-9_-]""", """[\w-]""".asciiPatternString()) - assertEquals("""[^A-Za-z0-9_]""", """\W""".asciiPatternString()) - assertEquals("""[0-9]""", """\d""".asciiPatternString()) - assertEquals("""[0-9:-]""", """[\d:-]""".asciiPatternString()) - assertEquals("""[^0-9]""", """\D""".asciiPatternString()) - - // 文字セットの中の \W \D は変換できないので素通しする - assertEquals("""[\W]""", """[\W]""".asciiPatternString()) - assertEquals("""[\D]""", """[\D]""".asciiPatternString()) - - // エスケープ文字の後に何もない場合も素通しする - assertEquals("""\""", """\""".asciiPatternString()) - - } - - @Test - fun testMisskeyMention() { - fun findMention(str : String) : String? { - val m = TootAccount.reMisskeyMentionMFM.matcher(str) - return if(m.find()) m.group(0) else null - } - assertEquals(null, findMention("")) - assertEquals(null, findMention("tateisu")) - assertEquals("@tateisu", findMention("@tateisu")) - assertEquals("@tateisu", findMention("@tateisuほげ")) - assertEquals("@tateisu@mastodon.juggler.jp", findMention("@tateisu@mastodon.juggler.jp")) - assertEquals("@tateisu@mastodon.juggler.jp", findMention("@tateisu@mastodon.juggler.jpほげ")) - assertEquals("@tateisu", findMention("@tateisu@マストドン3.juggler.jp")) - assertEquals( - "@tateisu@xn--3-pfuzbe6htf.juggler.jp", - findMention("@tateisu@xn--3-pfuzbe6htf.juggler.jp") - ) - } - - @Test - fun testMastodonMention() { - fun findMention(str : String) : String? { - val m = TootAccount.reCountMention.matcher(str) - return if(m.find()) m.group(0) else null - } - assertEquals(null, findMention("")) - assertEquals(null, findMention("tateisu")) - assertEquals("@tateisu", findMention("@tateisu")) - assertEquals("@tateisu", findMention("@tateisuほげ")) - assertEquals("@tateisu@mastodon.juggler.jp", findMention("@tateisu@mastodon.juggler.jp")) - assertEquals("@tateisu@mastodon.juggler.jp", findMention("@tateisu@mastodon.juggler.jpほげ")) - assertEquals("@tateisu@マストドン3.juggler.jp", findMention("@tateisu@マストドン3.juggler.jp")) - assertEquals( - "@tateisu@xn--3-pfuzbe6htf.juggler.jp", - findMention("@tateisu@xn--3-pfuzbe6htf.juggler.jp") - ) - } - } \ No newline at end of file diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt index 8ec501a0..8a8b0265 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt @@ -1,7 +1,7 @@ package jp.juggler.subwaytooter -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 import jp.juggler.subwaytooter.api.TootApiCallback import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.entity.Host diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/WordTrieTreeTest.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/WordTrieTreeTest.kt index 852007d2..a39770af 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/WordTrieTreeTest.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/WordTrieTreeTest.kt @@ -1,15 +1,12 @@ package jp.juggler.subwaytooter import androidx.test.runner.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - import jp.juggler.util.CharacterGroup import jp.juggler.util.WordTrieTree - import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull +import org.junit.Test +import org.junit.runner.RunWith /** * Instrumentation test, which will execute on an Android device. @@ -18,159 +15,199 @@ import org.junit.Assert.assertNotNull */ @RunWith(AndroidJUnit4::class) class WordTrieTreeTest { - - - companion object { - - private val whitespace_chars = charArrayOf(0x0009.toChar(), 0x000A.toChar(), 0x000B.toChar(), 0x000C.toChar(), 0x000D.toChar(), 0x001C.toChar(), 0x001D.toChar(), 0x001E.toChar(), 0x001F.toChar(), 0x0020.toChar(), 0x0085.toChar(), 0x00A0.toChar(), 0x1680.toChar(), 0x180E.toChar(), 0x2000.toChar(), 0x2001.toChar(), 0x2002.toChar(), 0x2003.toChar(), 0x2004.toChar(), 0x2005.toChar(), 0x2006.toChar(), 0x2007.toChar(), 0x2008.toChar(), 0x2009.toChar(), 0x200A.toChar(), 0x200B.toChar(), 0x200C.toChar(), 0x200D.toChar(), 0x2028.toChar(), 0x2029.toChar(), 0x202F.toChar(), 0x205F.toChar(), 0x2060.toChar(), 0x3000.toChar(), 0x3164.toChar(), 0xFEFF.toChar()) - } - - @Test - @Throws(Exception::class) - fun testCharacterGroupTokenizer() { - - - val whitespace = String(whitespace_chars) - val whitespace_len = whitespace.length - var id : Int - - run { - // トークナイザーに空白だけの文字列を与えたら、next() 一回で終端になる。offsetは0のままである。 - val tokenizer = CharacterGroup.Tokenizer().reset(whitespace, 0, whitespace.length) - id = tokenizer.next() - assertEquals(CharacterGroup.END, id) - assertEquals(0, tokenizer.offset.toLong()) - } - - run { - // トークナイザーに 空白+ABC+空白 を与えたら、A,B,C,終端になる。 - val strTest = whitespace + "ABC" + whitespace - val tokenizer = CharacterGroup.Tokenizer().reset(strTest, 0, strTest.length) - // - id = tokenizer.next() + + + companion object { + + private val whitespace_chars = charArrayOf( + 0x0009.toChar(), + 0x000A.toChar(), + 0x000B.toChar(), + 0x000C.toChar(), + 0x000D.toChar(), + 0x001C.toChar(), + 0x001D.toChar(), + 0x001E.toChar(), + 0x001F.toChar(), + 0x0020.toChar(), + 0x0085.toChar(), + 0x00A0.toChar(), + 0x1680.toChar(), + 0x180E.toChar(), + 0x2000.toChar(), + 0x2001.toChar(), + 0x2002.toChar(), + 0x2003.toChar(), + 0x2004.toChar(), + 0x2005.toChar(), + 0x2006.toChar(), + 0x2007.toChar(), + 0x2008.toChar(), + 0x2009.toChar(), + 0x200A.toChar(), + 0x200B.toChar(), + 0x200C.toChar(), + 0x200D.toChar(), + 0x2028.toChar(), + 0x2029.toChar(), + 0x202F.toChar(), + 0x205F.toChar(), + 0x2060.toChar(), + 0x3000.toChar(), + 0x3164.toChar(), + 0xFEFF.toChar() + ) + } + + @Test + @Throws(Exception::class) + fun testCharacterGroupTokenizer() { + + + val whitespace = String(whitespace_chars) + val whitespace_len = whitespace.length + var id: Int + + run { + // トークナイザーに空白だけの文字列を与えたら、next() 一回で終端になる。offsetは0のままである。 + val tokenizer = CharacterGroup.Tokenizer().reset(whitespace, 0, whitespace.length) + id = tokenizer.next() + assertEquals(CharacterGroup.END, id) + assertEquals(0, tokenizer.offset.toLong()) + } + + run { + // トークナイザーに 空白+ABC+空白 を与えたら、A,B,C,終端になる。 + val strTest = whitespace + "ABC" + whitespace + val tokenizer = CharacterGroup.Tokenizer().reset(strTest, 0, strTest.length) + // + id = tokenizer.next() assertEquals('A'.code, id) - assertEquals((whitespace_len + 1), tokenizer.offset) // offset は Aの次の位置になる - // - id = tokenizer.next() + assertEquals((whitespace_len + 1), tokenizer.offset) // offset は Aの次の位置になる + // + id = tokenizer.next() assertEquals('B'.code, id) - assertEquals((whitespace_len + 2).toLong(), tokenizer.offset.toLong()) - // - id = tokenizer.next() + assertEquals((whitespace_len + 2).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() assertEquals('C'.code, id) - assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) - // - id = tokenizer.next() - assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) // offsetはCの次の位置のまま - assertEquals(CharacterGroup.END, id) - } - - run { - // トークナイザーに 空白+abc+空白 を与えたら、A,B,C,終端になる。 - val strTest = whitespace + "abc" + whitespace - val tokenizer = CharacterGroup.Tokenizer().reset(strTest, 0, strTest.length) - // - id = tokenizer.next() - assertEquals((whitespace_len + 1).toLong(), tokenizer.offset.toLong()) + assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() + assertEquals( + (whitespace_len + 3).toLong(), + tokenizer.offset.toLong() + ) // offsetはCの次の位置のまま + assertEquals(CharacterGroup.END, id) + } + + run { + // トークナイザーに 空白+abc+空白 を与えたら、A,B,C,終端になる。 + val strTest = whitespace + "abc" + whitespace + val tokenizer = CharacterGroup.Tokenizer().reset(strTest, 0, strTest.length) + // + id = tokenizer.next() + assertEquals((whitespace_len + 1).toLong(), tokenizer.offset.toLong()) assertEquals('A'.code, id) - // - id = tokenizer.next() - assertEquals((whitespace_len + 2).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() + assertEquals((whitespace_len + 2).toLong(), tokenizer.offset.toLong()) assertEquals('B'.code, id) - // - id = tokenizer.next() - assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() + assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) assertEquals('C'.code, id) - // - id = tokenizer.next() - assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) - assertEquals(CharacterGroup.END, id) - } - - run { - // トークナイザーに 空白+ABC+空白 を与えたら、A,B,C,終端になる。 - val strTest = whitespace + "ABC" + whitespace - val tokenizer = CharacterGroup.Tokenizer().reset(strTest, 0, strTest.length) - // - id = tokenizer.next() - assertEquals((whitespace_len + 1).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() + assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) + assertEquals(CharacterGroup.END, id) + } + + run { + // トークナイザーに 空白+ABC+空白 を与えたら、A,B,C,終端になる。 + val strTest = whitespace + "ABC" + whitespace + val tokenizer = CharacterGroup.Tokenizer().reset(strTest, 0, strTest.length) + // + id = tokenizer.next() + assertEquals((whitespace_len + 1).toLong(), tokenizer.offset.toLong()) assertEquals('A'.code, id) - // - id = tokenizer.next() - assertEquals((whitespace_len + 2).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() + assertEquals((whitespace_len + 2).toLong(), tokenizer.offset.toLong()) assertEquals('B'.code, id) - // - id = tokenizer.next() - assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() + assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) assertEquals('C'.code, id) - // - id = tokenizer.next() - assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) - assertEquals(CharacterGroup.END, id) - } - - run { - // トークナイザーに 空白+abc+空白 を与えたら、A,B,C,終端になる。 - val strTest = whitespace + "abc" + whitespace - val tokenizer = CharacterGroup.Tokenizer().reset(strTest, 0, strTest.length) - // - id = tokenizer.next() - assertEquals((whitespace_len + 1).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() + assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) + assertEquals(CharacterGroup.END, id) + } + + run { + // トークナイザーに 空白+abc+空白 を与えたら、A,B,C,終端になる。 + val strTest = whitespace + "abc" + whitespace + val tokenizer = CharacterGroup.Tokenizer().reset(strTest, 0, strTest.length) + // + id = tokenizer.next() + assertEquals((whitespace_len + 1).toLong(), tokenizer.offset.toLong()) assertEquals('A'.code, id) - // - id = tokenizer.next() - assertEquals((whitespace_len + 2).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() + assertEquals((whitespace_len + 2).toLong(), tokenizer.offset.toLong()) assertEquals('B'.code, id) - // - id = tokenizer.next() - assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) + // + id = tokenizer.next() + assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) assertEquals('C'.code, id) - // - id = tokenizer.next() - assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) - assertEquals(CharacterGroup.END, id) - } - } - - @Test - @Throws(Exception::class) - fun testWordTrieTree() { - - var strTest : String - - run { - val trie = WordTrieTree() - trie.add("") - trie.add(" ") // 単語の側に空白があっても無視される - trie.add("ABC") - trie.add("abc") - trie.add("abcdef") - trie.add("bbb") - trie.add("C C C") // 単語の側に空白があっても無視される - trie.add("ccc") - - // 空文字列や空白を登録してもマッチしない - // 登録していない単語にマッチしない - assertEquals(false, trie.matchShort("ZZZ")) - - // 登録した文字列にマッチする - assertEquals(true, trie.matchShort("abc")) - assertEquals(true, trie.matchShort("abcdef")) - - // 単語の間に空白があってもマッチする - strTest = "///abcdef///a b c///bb b///c cc " - val list = trie.matchList(strTest) - assertNotNull(list) - assertEquals(4, list !!.size.toLong()) - assertEquals("abcdef", list[0].word) // abcよりもabcdefを優先してマッチする - assertEquals(3, list[0].start.toLong()) // 元テキスト中でマッチした位置を取得できる - assertEquals(9, list[0].end.toLong()) - assertEquals("ABC", list[1].word) // 文字種が違っても同一とみなす単語の場合、先に登録した方にマッチする - assertEquals("bbb", list[2].word) - assertEquals("C C C", list[3].word) // 文字種が違っても同一とみなす単語の場合、先に登録した方にマッチする - assertEquals(27, list[3].start.toLong()) // 元テキスト中でマッチした位置を取得できる - assertEquals(31, list[3].end.toLong()) - assertEquals(33, strTest.length.toLong()) // 末尾の空白はマッチ範囲には含まれない - } - } + // + id = tokenizer.next() + assertEquals((whitespace_len + 3).toLong(), tokenizer.offset.toLong()) + assertEquals(CharacterGroup.END, id) + } + } + + @Test + @Throws(Exception::class) + fun testWordTrieTree() { + + var strTest: String + + run { + val trie = WordTrieTree() + trie.add("") + trie.add(" ") // 単語の側に空白があっても無視される + trie.add("ABC") + trie.add("abc") + trie.add("abcdef") + trie.add("bbb") + trie.add("C C C") // 単語の側に空白があっても無視される + trie.add("ccc") + + // 空文字列や空白を登録してもマッチしない + // 登録していない単語にマッチしない + assertEquals(false, trie.matchShort("ZZZ")) + + // 登録した文字列にマッチする + assertEquals(true, trie.matchShort("abc")) + assertEquals(true, trie.matchShort("abcdef")) + + // 単語の間に空白があってもマッチする + strTest = "///abcdef///a b c///bb b///c cc " + val list = trie.matchList(strTest) + assertNotNull(list) + assertEquals(4, list!!.size.toLong()) + assertEquals("abcdef", list[0].word) // abcよりもabcdefを優先してマッチする + assertEquals(3, list[0].start.toLong()) // 元テキスト中でマッチした位置を取得できる + assertEquals(9, list[0].end.toLong()) + assertEquals("ABC", list[1].word) // 文字種が違っても同一とみなす単語の場合、先に登録した方にマッチする + assertEquals("bbb", list[2].word) + assertEquals("C C C", list[3].word) // 文字種が違っても同一とみなす単語の場合、先に登録した方にマッチする + assertEquals(27, list[3].start.toLong()) // 元テキスト中でマッチした位置を取得できる + assertEquals(31, list[3].end.toLong()) + assertEquals(33, strTest.length.toLong()) // 末尾の空白はマッチ範囲には含まれない + } + } } diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt index 807cf393..761eb3e2 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt @@ -1,220 +1,236 @@ package jp.juggler.subwaytooter.api +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 -import android.test.mock.MockContext import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.JsonObject import jp.juggler.util.jsonObject - +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull - @RunWith(AndroidJUnit4::class) class TestDuplicateMap { - - private val parser = TootParser( - MockContext(), - SavedAccount( - db_id = 1, - acctArg = "user1@host1", - apiHostArg = null - ) - ) - - private val generatedItems = ArrayList() - - private fun genStatus( - parser : TootParser, - accountJson : JsonObject, - statusId : String, - uri : String?, - url : String? - ):TootStatus{ - val itemJson = jsonObject{ - put("account", accountJson) - put("id", statusId) - if(uri != null) put("uri", uri) - if(url != null) put("url", url) - } - - return TootStatus(parser,itemJson) - } - - private fun checkStatus( - map : DuplicateMap, - parser : TootParser, - accountJson : JsonObject, - statusId : String, - uri : String?, - url : String? - ) { - val item = genStatus(parser,accountJson,statusId,uri,url) - assertNotNull(item) - generatedItems.add(item) - assertEquals(false, map.isDuplicate(item)) - assertEquals(true, map.isDuplicate(item)) - } - - private fun testDuplicateStatus() { - - val account1Json = jsonObject{ - put("username", "user1") - put("acct", "user1") - put("id", 1L) - put("url", "http://${parser.apiHost}/@user1") - } - - val account1 = TootAccount(parser,account1Json) - assertNotNull(account1) - - val map = DuplicateMap() - - // 普通のステータス - checkStatus( - map, - parser, - account1Json, - "1", - "http://${parser.apiHost}/@${account1.username}/1", - "http://${parser.apiHost}/@${account1.username}/1" - ) - // 別のステータス - checkStatus( - map, - parser, - account1Json, - "2", - "http://${parser.apiHost}/@${account1.username}/2", - "http://${parser.apiHost}/@${account1.username}/2" - ) - // 今度はuriがない - checkStatus( - map, - parser, - account1Json, - "3", - null, // "http://${parser.accessHost}/@${account1.username}/3", - "http://${parser.apiHost}/@${account1.username}/3" - ) - // 今度はuriとURLがない - checkStatus( - map, - parser, - account1Json, - "4", - null, // "http://${parser.accessHost}/@${account1.username}/4", - null //"http://${parser.accessHost}/@${account1.username}/4" - ) - // 今度はIDがおかしい - checkStatus( - map, - parser, - account1Json, - "", - null, // "http://${parser.accessHost}/@${account1.username}/4", - null //"http://${parser.accessHost}/@${account1.username}/4" - ) - - } - - - private fun checkNotification( - map : DuplicateMap, - parser : TootParser, - id : Long - ) { - val itemJson = JsonObject() - - itemJson.apply { - put("type", TootNotification.TYPE_MENTION) - put("id", id) - } - - val item = TootNotification( parser,itemJson ) - assertNotNull(item) - generatedItems.add(item) - assertEquals(false, map.isDuplicate(item)) - assertEquals(true, map.isDuplicate(item)) - } - - private fun testDuplicateNotification() { - val map = DuplicateMap() - checkNotification(map,parser,0L) - checkNotification(map,parser,1L) - checkNotification(map,parser,2L) - checkNotification(map,parser,3L) - } - - private fun checkReport( - map : DuplicateMap, - id : Long - ) { - val item = TootReport(JsonObject().apply{ - put("id",id) - put("action_taken","eat") - }) - - assertNotNull(item) - generatedItems.add(item) - assertEquals(false, map.isDuplicate(item)) - assertEquals(true, map.isDuplicate(item)) - } - - - private fun testDuplicateReport() { - val map = DuplicateMap() - checkReport(map,0L) - checkReport(map,1L) - checkReport(map,2L) - checkReport(map,3L) - } - - private fun checkAccount( - map : DuplicateMap, - parser : TootParser, - id : Long - ) { - val itemJson = JsonObject() - itemJson.apply { - put("username", "user$id") - put("acct", "user$id") - put("id", id) - put("url", "http://${parser.apiHost}/@user$id") - } - - val item = TootAccountRef.notNull(parser,TootAccount(parser,itemJson)) - assertNotNull(item) - generatedItems.add(item) - assertEquals(false, map.isDuplicate(item)) - assertEquals(true, map.isDuplicate(item)) // 二回目はtrueになる - } - - private fun testDuplicateAccount() { - val map = DuplicateMap() - checkAccount(map,parser,0L) - checkAccount(map,parser,1L) - checkAccount(map,parser,2L) - checkAccount(map,parser,3L) - } - - @Test fun testFilterList(){ - generatedItems.clear() - testDuplicateStatus() - testDuplicateNotification() - testDuplicateReport() - testDuplicateAccount() - - val map = DuplicateMap() + private val parser = TootParser( + InstrumentationRegistry.getInstrumentation().targetContext, + SavedAccount( + db_id = 1, + acctArg = "user1@host1", + apiHostArg = null + ) + ) - val dst = map.filterDuplicate( generatedItems) - assertEquals( generatedItems.size,dst.size) + private fun genStatus( + parser: TootParser, + accountJson: JsonObject, + statusId: String, + uri: String, + url: String? + ): TootStatus { + val itemJson = jsonObject { + put("account", accountJson) + put("id", statusId) + put("uri", uri) + if (url != null) put("url", url) + } - val dst2 = map.filterDuplicate( generatedItems) - assertEquals( 0,dst2.size) - } + return TootStatus(parser, itemJson) + } + + private fun testDuplicateStatus(): ArrayList { + val generatedItems = ArrayList() + fun checkStatus( + map: DuplicateMap, + parser: TootParser, + accountJson: JsonObject, + statusId: String, + uri: String, + url: String? + ) { + val item = genStatus(parser, accountJson, statusId, uri, url) + assertNotNull(item) + generatedItems.add(item) + assertEquals(false, map.isDuplicate(item)) + assertEquals(true, map.isDuplicate(item)) + } + + + val account1Json = jsonObject { + put("username", "user1") + put("acct", "user1") + put("id", 1L) + put("url", "http://${parser.apiHost}/@user1") + } + + val account1 = TootAccount(parser, account1Json) + assertNotNull(account1) + + val map = DuplicateMap() + + // 普通のステータス + checkStatus( + map, + parser, + account1Json, + "s1", + "http://${parser.apiHost}/@${account1.username}/1", + "http://${parser.apiHost}/@${account1.username}/1" + ) + // 別のステータス + checkStatus( + map, + parser, + account1Json, + "s2", + "http://${parser.apiHost}/@${account1.username}/2", + "http://${parser.apiHost}/@${account1.username}/2" + ) + // URIは必須になったので、URIなしのテストは行わない + // 今度はURLがない + checkStatus( + map, + parser, + account1Json, + "s4", + "http://${parser.apiHost}/@${account1.username}/4", + null //"http://${parser.apiHost}/@${account1.username}/4" + ) + // 今度はIDがおかしい + checkStatus( + map, + parser, + account1Json, + "", + "http://${parser.apiHost}/@${account1.username}/5", + "http://${parser.apiHost}/@${account1.username}/5" + ) + return generatedItems + } + + + private fun testDuplicateNotification(): ArrayList { + val generatedItems = ArrayList() + fun checkNotification( + map: DuplicateMap, + parser: TootParser, + id: String + ) { + val itemJson = JsonObject() + + itemJson.apply { + put("type", TootNotification.TYPE_MENTION) + put("id", id) + } + + val item = TootNotification(parser, itemJson) + assertNotNull(item) + generatedItems.add(item) + assertEquals(false, map.isDuplicate(item)) + assertEquals(true, map.isDuplicate(item)) + } + + val map = DuplicateMap() + checkNotification(map, parser, "n0") + checkNotification(map, parser, "n1") + checkNotification(map, parser, "n2") + checkNotification(map, parser, "n3") + return generatedItems + } + + private fun testDuplicateReport(): ArrayList { + val generatedItems = ArrayList() + fun checkReport( + map: DuplicateMap, + id: String + ) { + val item = TootReport(JsonObject().apply { + put("id", id) + put("action_taken", "eat") + }) + + assertNotNull(item) + generatedItems.add(item) + assertEquals(false, map.isDuplicate(item)) + assertEquals(true, map.isDuplicate(item)) + } + + + val map = DuplicateMap() + checkReport(map, "r0") + checkReport(map, "r1") + checkReport(map, "r2") + checkReport(map, "r3") + return generatedItems + } + + + private fun testDuplicateAccount(): ArrayList { + val generatedItems = ArrayList() + fun checkAccount( + map: DuplicateMap, + parser: TootParser, + id: String + ) { + + val itemJson = JsonObject() + itemJson.apply { + put("username", "user$id") + put("acct", "user$id") + put("id", id) + put("url", "http://${parser.apiHost}/@user$id") + } + + val item = TootAccountRef.notNull(parser, TootAccount(parser, itemJson)) + assertNotNull(item) + generatedItems.add(item) + assertEquals(false, map.isDuplicate(item)) + assertEquals(true, map.isDuplicate(item)) // 二回目はtrueになる + } + + val map = DuplicateMap() + checkAccount(map, parser, "a0") + checkAccount(map, parser, "a1") + checkAccount(map, parser, "a2") + checkAccount(map, parser, "a3") + return generatedItems + } + + @Test + fun testFilterList() { + val statusItems = testDuplicateStatus() + val notiItems = testDuplicateNotification() + val reportItems = testDuplicateReport() + val accountItems = testDuplicateAccount() + + var map = DuplicateMap() + var dst = map.filterDuplicate(statusItems) + assertEquals("statusItems", statusItems.size, dst.size) + + map = DuplicateMap() + dst = map.filterDuplicate(notiItems) + assertEquals("notiItems", notiItems.size, dst.size) + + map = DuplicateMap() + dst = map.filterDuplicate(reportItems) + assertEquals("reportItems", reportItems.size, dst.size) + + map = DuplicateMap() + dst = map.filterDuplicate(accountItems) + assertEquals("accountItems", accountItems.size, dst.size) + + val totalItems = ArrayList() + totalItems.addAll(statusItems) + totalItems.addAll(notiItems) + totalItems.addAll(reportItems) + totalItems.addAll(accountItems) + map = DuplicateMap() + dst = map.filterDuplicate(totalItems) + assertEquals("totalItems", totalItems.size, dst.size) + + val dst2 = map.filterDuplicate(totalItems) + assertEquals(0, dst2.size) + } } diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt index 370e9a7c..bea93417 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt @@ -2,7 +2,7 @@ package jp.juggler.subwaytooter.api -import androidx.test.InstrumentationRegistry +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.TootInstance @@ -28,29 +28,53 @@ import java.util.concurrent.atomic.AtomicReference @RunWith(AndroidJUnit4::class) class TestTootApiClient { - private val appContext = InstrumentationRegistry.getTargetContext()!! + private val appContext = InstrumentationRegistry.getInstrumentation().targetContext!! class SimpleHttpClientMock( - private val responseGenerator: (request: Request) -> Response, - val webSocketGenerator: (request: Request, ws_listener: WebSocketListener) -> WebSocket - ) : SimpleHttpClient { + private val responseGenerator: (request: Request) -> Response, + val webSocketGenerator: (request: Request, ws_listener: WebSocketListener) -> WebSocket + ) : SimpleHttpClient { override var onCallCreated: (Call) -> Unit = {} // override var currentCallCallback : CurrentCallCallback? = null - override suspend fun getResponse(request: Request, tmpOkhttpClient: OkHttpClient?): Response { + override suspend fun getResponse( + request: Request, + tmpOkhttpClient: OkHttpClient? + ): Response { return responseGenerator(request) } override fun getWebSocket( - request: Request, - webSocketListener: WebSocketListener - ): WebSocket { + request: Request, + webSocketListener: WebSocketListener + ): WebSocket { return webSocketGenerator(request, webSocketListener) } } + private fun assertOneOf(actual: T?, vararg expect: T?) { + if (!expect.any { it == actual }) { + fail("actual=$actual, expected = one of [${expect.joinToString(", ")}]") + } + } + + private fun assertParsingResponse(callback: ProgressRecordTootApiCallback) { + assertOneOf( + callback.progressString, + "Parsing response…", + "応答の解析中…" + ) + } + private fun assertReading(callback: ProgressRecordTootApiCallback,path:String){ + assertOneOf( + callback.progressString, + "Reading: GET $path", + "読込中: GET $path", + ) + } + private fun requestBodyString(request: Request?): String? { try { val copyBody = request?.newBuilder()?.build()?.body ?: return null @@ -65,174 +89,174 @@ class TestTootApiClient { private fun createHttpClientNormal(): SimpleHttpClient { return SimpleHttpClientMock( - responseGenerator = { request: Request -> + responseGenerator = { request: Request -> - val bodyString = requestBodyString(request) + val bodyString = requestBodyString(request) - when (request.url.encodedPath) { + when (request.url.encodedPath) { - // クライアント登録 - "/api/v1/apps" -> Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("status-message") - .body( - """{"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"DUMMY_ID","client_secret":"DUMMY_SECRET"}""" - .toResponseBody(MEDIA_TYPE_JSON) - ) - .build() + // クライアント登録 + "/api/v1/apps" -> Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("status-message") + .body( + """{"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"DUMMY_ID","client_secret":"DUMMY_SECRET"}""" + .toResponseBody(MEDIA_TYPE_JSON) + ) + .build() - // client credentialの検証 - "/api/v1/apps/verify_credentials" -> Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("status-message") - .body( - """{"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"DUMMY_ID","client_secret":"DUMMY_SECRET"}""" - .toResponseBody(MEDIA_TYPE_JSON) - ) - .build() + // client credentialの検証 + "/api/v1/apps/verify_credentials" -> Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("status-message") + .body( + """{"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"DUMMY_ID","client_secret":"DUMMY_SECRET"}""" + .toResponseBody(MEDIA_TYPE_JSON) + ) + .build() - "/oauth/token" -> when { - // client credential の作成 - bodyString?.contains("grant_type=client_credentials") == true -> { - Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("status-message") - .body( - """{"access_token":"DUMMY_CLIENT_CREDENTIAL"}""".toResponseBody( - MEDIA_TYPE_JSON - ) - ) - .build() + "/oauth/token" -> when { + // client credential の作成 + bodyString?.contains("grant_type=client_credentials") == true -> { + Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("status-message") + .body( + """{"access_token":"DUMMY_CLIENT_CREDENTIAL"}""".toResponseBody( + MEDIA_TYPE_JSON + ) + ) + .build() - } - // アクセストークンの作成 - bodyString?.contains("grant_type=authorization_code") == true -> { - Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("status-message") - .body( - """{"access_token":"DUMMY_ACCESS_TOKEN"}""".toResponseBody( - MEDIA_TYPE_JSON - ) - ) - .build() - } + } + // アクセストークンの作成 + bodyString?.contains("grant_type=authorization_code") == true -> { + Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("status-message") + .body( + """{"access_token":"DUMMY_ACCESS_TOKEN"}""".toResponseBody( + MEDIA_TYPE_JSON + ) + ) + .build() + } - else -> { - createResponseErrorCode() - } - } - // ログインユーザの情報 - "/api/v1/accounts/verify_credentials" -> { - val instance = request.url.host - val account1Json = JsonObject() - account1Json.apply { - put("username", "user1") - put("acct", "user1") - put("id", 1L) - put("url", "http://$instance/@user1") - } + else -> { + createResponseErrorCode() + } + } + // ログインユーザの情報 + "/api/v1/accounts/verify_credentials" -> { + val instance = request.url.host + val account1Json = JsonObject() + account1Json.apply { + put("username", "user1") + put("acct", "user1") + put("id", 1L) + put("url", "http://$instance/@user1") + } - Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("status-message") - .body(account1Json.toString().toResponseBody(MEDIA_TYPE_JSON)) - .build() - } - // インスタンス情報 - "/api/v1/instance" -> Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("status-message") - .body(JsonObject().apply { - put("uri", "http://${request.url.host}/") - put("title", "dummy instance") - put("description", "dummy description") - put("version", "0.0.1") - }.toString().toResponseBody(MEDIA_TYPE_JSON)) - .build() + Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("status-message") + .body(account1Json.toString().toResponseBody(MEDIA_TYPE_JSON)) + .build() + } + // インスタンス情報 + "/api/v1/instance" -> Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("status-message") + .body(JsonObject().apply { + put("uri", "http://${request.url.host}/") + put("title", "dummy instance") + put("description", "dummy description") + put("version", "0.0.1") + }.toString().toResponseBody(MEDIA_TYPE_JSON)) + .build() - // 公開タイムライン - "/api/v1/timelines/public" -> { - val instance = request.url.host + // 公開タイムライン + "/api/v1/timelines/public" -> { + val instance = request.url.host - val username = "user1" + val username = "user1" - val account1Json = JsonObject() - account1Json.apply { - put("username", username) - put("acct", username) - put("id", 1L) - put("url", "http://$instance/@$username") - } + val account1Json = JsonObject() + account1Json.apply { + put("username", username) + put("acct", username) + put("id", 1L) + put("url", "http://$instance/@$username") + } - val array = jsonArray { - for (i in 0 until 10) { - add(jsonObject { - put("account", account1Json) - put("id", i.toLong()) - put("uri", "https://$instance/@$username/$i") - put("url", "https://$instance/@$username/$i") - }) - } + val array = jsonArray { + for (i in 0 until 10) { + add(jsonObject { + put("account", account1Json) + put("id", i.toLong()) + put("uri", "https://$instance/@$username/$i") + put("url", "https://$instance/@$username/$i") + }) + } - } + } - Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("status-message") - .body(array.toString().toResponseBody(MEDIA_TYPE_JSON)) - .build() - } + Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("status-message") + .body(array.toString().toResponseBody(MEDIA_TYPE_JSON)) + .build() + } - else -> - Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("status-message") - .body(request.url.toString().toResponseBody(mediaTypeTextPlain)) - .build() - } + else -> + Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("status-message") + .body(request.url.toString().toResponseBody(mediaTypeTextPlain)) + .build() + } - }, + }, - webSocketGenerator = { request: Request, _: WebSocketListener -> - object : WebSocket { - override fun queueSize(): Long = 4096L + webSocketGenerator = { request: Request, _: WebSocketListener -> + object : WebSocket { + override fun queueSize(): Long = 4096L - override fun send(text: String): Boolean = true + override fun send(text: String): Boolean = true - override fun send(bytes: ByteString): Boolean = true + override fun send(bytes: ByteString): Boolean = true - override fun close(code: Int, reason: String?): Boolean = true + override fun close(code: Int, reason: String?): Boolean = true - override fun cancel() = Unit + override fun cancel() = Unit - override fun request(): Request = request - } - } - ) + override fun request(): Request = request + } + } + ) } private fun createHttpClientNotImplemented(): SimpleHttpClient { return SimpleHttpClientMock( - responseGenerator = { throw NotImplementedError() }, - webSocketGenerator = { _, _ -> throw NotImplementedError() } - ) + responseGenerator = { throw NotImplementedError() }, + webSocketGenerator = { _, _ -> throw NotImplementedError() } + ) } class ProgressRecordTootApiCallback : TootApiCallback { @@ -299,12 +323,12 @@ class TestTootApiClient { .code(200) .message("status-message") .body( - object : ResponseBody() { - override fun contentLength() = 10L - override fun contentType(): MediaType = MEDIA_TYPE_JSON - override fun source(): BufferedSource = error("ExceptionBody") - } - ) + object : ResponseBody() { + override fun contentLength() = 10L + override fun contentType(): MediaType = MEDIA_TYPE_JSON + override fun source(): BufferedSource = error("ExceptionBody") + } + ) .build() private val strJsonArray1 = """["A!"]""" @@ -384,7 +408,7 @@ class TestTootApiClient { .body("Error!".toResponseBody("text/plain".toMediaType())) .build() - message =TootApiClient.simplifyErrorHtml(response) + message = TootApiClient.simplifyErrorHtml(response) assertEquals("Error!", message) // empty body @@ -400,7 +424,7 @@ class TestTootApiClient { .body("".toResponseBody("text/plain".toMediaType())) .build() - message = TootApiClient.simplifyErrorHtml(response=response,caption="caption" ) + message = TootApiClient.simplifyErrorHtml(response = response, caption = "caption") assertEquals("", message) } @@ -424,7 +448,7 @@ class TestTootApiClient { .message("This is test") .build() - message = TootApiClient.formatResponse(response,"caption") + message = TootApiClient.formatResponse(response, "caption") assertEquals("(HTTP 500 This is test) caption", message) @@ -441,7 +465,7 @@ class TestTootApiClient { .body("""{"error":"Error!"}""".toResponseBody(MEDIA_TYPE_JSON)) .build() - message = TootApiClient.formatResponse(response,"caption") + message = TootApiClient.formatResponse(response, "caption") assertEquals("Error! (HTTP 500 status-message) caption", message) // json error (after reading body) @@ -460,8 +484,8 @@ class TestTootApiClient { bodyString = response.body?.string() - message = TootApiClient.formatResponse(response,"caption",bodyString) - assertEquals("Error! (HTTP 500 status-message) caption", message) + message = TootApiClient.formatResponse(response, "caption", bodyString) + assertEquals("(HTTP 500 status-message) caption", message) // without status message request = Request.Builder() @@ -478,8 +502,12 @@ class TestTootApiClient { bodyString = response.body?.string() - message = TootApiClient.formatResponse(response = response,caption = "caption",bodyString = bodyString) - assertEquals("Error! (HTTP 500) caption", message) + message = TootApiClient.formatResponse( + response = response, + caption = "caption", + bodyString = bodyString + ) + assertEquals("(HTTP 500) caption", message) } @Test @@ -491,27 +519,27 @@ class TestTootApiClient { var progressMax: Int? = null val client = TootApiClient( - appContext, - httpClient = createHttpClientNotImplemented(), - callback = object : TootApiCallback { - override val isApiCancelled: Boolean - get() { - ++flag - return true - } + appContext, + httpClient = createHttpClientNotImplemented(), + callback = object : TootApiCallback { + override val isApiCancelled: Boolean + get() { + ++flag + return true + } - override suspend fun publishApiProgress(s: String) { - ++flag - progressString = s - } + override suspend fun publishApiProgress(s: String) { + ++flag + progressString = s + } - override suspend fun publishApiProgressRatio(value: Int, max: Int) { - ++flag - progressValue = value - progressMax = max - } - } - ) + override suspend fun publishApiProgressRatio(value: Int, max: Int) { + ++flag + progressValue = value + progressMax = max + } + } + ) val isApiCancelled = client.isApiCancelled client.publishApiProgress("testing") client.publishApiProgressRatio(50, 100) @@ -533,17 +561,21 @@ class TestTootApiClient { // 正常ケースではResponseが返ってくること run { val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) val result = TootApiResult.makeWithCaption("instance") assertEquals(null, result.error) callback.progressString = null val bOk = client.sendRequest(result) { requestSimple } assertEquals(true, bOk) - assertEquals("Acquiring: GET /", callback.progressString) + assertOneOf( + callback.progressString, + "Acquiring: GET /", + "取得中: GET /", + ) assertEquals(null, result.error) assertNotNull(result.response) } @@ -551,21 +583,26 @@ class TestTootApiClient { // httpClient.getResponseが例外を出す場合に対応できること run { val client = TootApiClient( - appContext, - httpClient = createHttpClientNotImplemented(), - callback = callback - ) + appContext, + httpClient = createHttpClientNotImplemented(), + callback = callback + ) val result = TootApiResult.makeWithCaption("instance") assertEquals(null, result.error) callback.progressString = null val bOk = client.sendRequest(result) { requestSimple } assertEquals(false, bOk) - assertEquals("Acquiring: GET /", callback.progressString) - assertEquals( - "instance: Network error.: NotImplementedError An operation is not implemented.", - result.error - ) + assertOneOf( + callback.progressString, + "Acquiring: GET /", + "取得中: GET /", + ) + assertOneOf( + result.error, + "instance: Network error.: NotImplementedError An operation is not implemented.", + "instance: 通信エラー。: NotImplementedError An operation is not implemented.", + ) assertNull(result.response) } @@ -573,17 +610,21 @@ class TestTootApiClient { // progressPath を指定したらpublishApiProgressに渡されること run { val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) val result = TootApiResult.makeWithCaption("instance") assertEquals(null, result.error) callback.progressString = null val bOk = client.sendRequest(result, progressPath = "XXX") { requestSimple } assertEquals(true, bOk) - assertEquals("Acquiring: GET XXX", callback.progressString) + assertOneOf( + callback.progressString, + "Acquiring: GET XXX", + "取得中: GET XXX", + ) assertEquals(null, result.error) assertNotNull(result.response) } @@ -595,10 +636,10 @@ class TestTootApiClient { runBlocking { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) // キャンセルされてたらnullを返すこと run { @@ -626,7 +667,7 @@ class TestTootApiClient { val bodyString = client.readBodyString(result) assertEquals(strJsonOk, bodyString) assertEquals(strJsonOk, result.bodyString) - assertEquals("Parsing response…", callback.progressString) + assertParsingResponse(callback) assertNull(result.error) assertNull(result.data) } @@ -642,7 +683,7 @@ class TestTootApiClient { val bodyString = client.readBodyString(result) assertEquals(null, bodyString) assertEquals(null, result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals("Error! (HTTP 500 status-message) instance", result.error) assertNull(result.data) } @@ -657,7 +698,7 @@ class TestTootApiClient { val bodyString = client.readBodyString(result) assertEquals("", bodyString) assertEquals("", result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals(null, result.error) assertNull(result.data) } @@ -672,7 +713,7 @@ class TestTootApiClient { val bodyString = client.readBodyString(result) assertEquals("", bodyString) assertEquals("", result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals(null, result.error) assertNull(result.data) } @@ -704,10 +745,10 @@ class TestTootApiClient { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) // キャンセルされてたらnullを返すこと run { @@ -736,7 +777,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(strJsonOk, result.string) assertEquals(strJsonOk, result.bodyString) - assertEquals("Parsing response…", callback.progressString) + assertParsingResponse(callback) assertNull(result.error) } // 正常レスポンスならJSONにエラーがあってもreadStringは関知しない @@ -750,7 +791,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(strJsonError, result.string) assertEquals(strJsonError, result.bodyString) - assertEquals("Parsing response…", callback.progressString) + assertParsingResponse(callback) assertNull(result.error) } @@ -765,7 +806,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(null, result.string) assertEquals(null, result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals("Error! (HTTP 500 status-message) instance", result.error) } @@ -780,7 +821,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(null, result.string) assertEquals(null, result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals("(no information) (HTTP 404 status-message) instance", result.error) assertNull(result.data) } @@ -796,7 +837,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals("", result.string) assertEquals("", result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals(null, result.error) assertEquals("", result.data) } @@ -811,7 +852,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(null, result.string) assertEquals(null, result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals("(no information) (HTTP 200 status-message) instance", result.error) assertNull(result.data) } @@ -824,10 +865,10 @@ class TestTootApiClient { runBlocking { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) // キャンセルされてたらnullを返すこと run { @@ -857,7 +898,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals("A!", result.jsonObject?.optString("a")) assertEquals(strJsonOk, result.bodyString) - assertEquals("Parsing response…", callback.progressString) + assertParsingResponse(callback) assertNull(result.error) } // 正常ケースでもjsonデータにerror項目があれば @@ -872,7 +913,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(null, result.data) assertEquals(strJsonError, result.bodyString) - assertEquals("Parsing response…", callback.progressString) + assertParsingResponse(callback) assertEquals("Error!", result.error) } @@ -886,7 +927,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(null, result.data) assertEquals(null, result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals("Error! (HTTP 500 status-message) instance", result.error) } @@ -901,7 +942,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(0, result.jsonObject?.size) assertEquals("", result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals(null, result.error) } @@ -915,7 +956,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(0, result.jsonObject?.size) assertEquals("", result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals(null, result.error) } @@ -929,7 +970,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(null, result.data) assertEquals(null, result.bodyString) - assertEquals("Reading: GET instance", callback.progressString) + assertReading(callback,"instance") assertEquals("(no information) (HTTP 200 status-message) instance", result.error) assertNull(result.data) } @@ -945,7 +986,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals("A!", result.jsonArray?.optString(0)) assertEquals(strJsonArray1, result.bodyString) - assertEquals("Parsing response…", callback.progressString) + assertParsingResponse(callback) assertNull(result.error) } @@ -961,7 +1002,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals("A!", result.jsonArray?.optString(0)) assertEquals(strJsonArray2, result.bodyString) - assertEquals("Parsing response…", callback.progressString) + assertParsingResponse(callback) assertNull(result.error) } @@ -976,7 +1017,7 @@ class TestTootApiClient { assertNotNull(r2) assertEquals("A!", result.jsonObject?.optString("a")) assertEquals(strJsonObject2, result.bodyString) - assertEquals("Parsing response…", callback.progressString) + assertParsingResponse(callback) assertNull(result.error) } // JSONじゃない @@ -990,8 +1031,12 @@ class TestTootApiClient { assertNotNull(r2) assertEquals(null, result.data) assertEquals(strPlainText, result.bodyString) - assertEquals("Parsing response…", callback.progressString) - assertEquals("API response is not JSON. Hello! (HTTP 200 status-message) https://dummy-url.net/", result.error) + assertParsingResponse(callback) + assertOneOf( + result.error, + "API response is not JSON. Hello! (HTTP 200 status-message) https://dummy-url.net/", + "APIの応答がJSONではありません Hello! (HTTP 200 status-message) https://dummy-url.net/", + ) } } } @@ -1001,10 +1046,10 @@ class TestTootApiClient { runBlocking { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) val instance = Host.parse("unit-test") client.apiHost = instance val clientName = "SubwayTooterUnitTest" @@ -1052,7 +1097,7 @@ class TestTootApiClient { // ブラウザからコールバックで受け取ったcodeを処理する val refToken = AtomicReference(null) - result = client.authentication2Mastodon(clientName, "DUMMY_CODE",refToken) + result = client.authentication2Mastodon(clientName, "DUMMY_CODE", refToken) jsonObject = result?.jsonObject assertNotNull(jsonObject) if (jsonObject == null) return@runBlocking @@ -1080,13 +1125,13 @@ class TestTootApiClient { runBlocking { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) val instance = Host.parse("unit-test") client.apiHost = instance - val (instanceInfo, instanceResult) = TootInstance.get(client) + val (instanceInfo, instanceResult) = TootInstance.get(client) assertNotNull(instanceInfo) assertNotNull(instanceResult) val json = instanceResult?.jsonObject @@ -1099,10 +1144,10 @@ class TestTootApiClient { runBlocking { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) val result = client.getHttp("http://juggler.jp/") val content = result?.string assertNotNull(content) @@ -1117,17 +1162,17 @@ class TestTootApiClient { tokenInfo["access_token"] = "DUMMY_ACCESS_TOKEN" val accessInfo = SavedAccount( - db_id = 1, - acctArg = "user1@host1", - apiHostArg = null, - token_info = tokenInfo - ) + db_id = 1, + acctArg = "user1@host1", + apiHostArg = null, + token_info = tokenInfo + ) val callback = ProgressRecordTootApiCallback() val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) client.account = accessInfo val result = client.request("/api/v1/timelines/public") println(result?.bodyString) @@ -1146,21 +1191,21 @@ class TestTootApiClient { } val accessInfo = SavedAccount( - db_id = 1, - acctArg = "user1@host1", - apiHostArg = null, - token_info = tokenInfo - ) + db_id = 1, + acctArg = "user1@host1", + apiHostArg = null, + token_info = tokenInfo + ) val callback = ProgressRecordTootApiCallback() val client = TootApiClient( - appContext, - httpClient = createHttpClientNormal(), - callback = callback - ) + appContext, + httpClient = createHttpClientNormal(), + callback = callback + ) client.account = accessInfo - val (_, ws) = client.webSocket("/api/v1/streaming/?stream=public:local", - object : WebSocketListener() { - }) + val (_, ws) = client.webSocket("/api/v1/streaming/?stream=public:local", + object : WebSocketListener() { + }) assertNotNull(ws) ws?.cancel() } diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt index cecdc885..50a6424b 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt @@ -1,269 +1,268 @@ package jp.juggler.subwaytooter.api.entity -import androidx.test.runner.AndroidJUnit4 import android.test.mock.MockContext +import androidx.test.runner.AndroidJUnit4 import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.JsonArray import jp.juggler.util.JsonObject -import jp.juggler.util.notEmptyOrThrow import jp.juggler.util.decodeJsonObject +import jp.juggler.util.notEmptyOrThrow import org.junit.Assert.* -import org.junit.runner.RunWith - import org.junit.Test +import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TestEntityUtils { - - class TestEntity(val s : String, val l : Long) : Mappable { - constructor(src : JsonObject) : this( - s = src.stringOrThrow("s"), - l = src.long("l") ?: 0L - ) - - @Suppress("UNUSED_PARAMETER") - constructor(parser : TootParser, src : JsonObject) : this( - s = src.stringOrThrow("s"), - l = src.long("l") ?: 0L - ) - - override val mapKey : String - get() = s - } - - @Test - fun testParseItem() { - assertEquals(null, parseItem(::TestEntity, null)) - - run { - val src = """{"s":null,"l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) - assertNull(item) - } - run { - val src = """{"s":"","l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) - assertNull(item) - } - run { - val src = """{"s":"A","l":null}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) - assertNotNull(item) - assertEquals(src.optString("s"), item?.s) - assertEquals(src.optLong("l"), item?.l) - } - run { - val src = """{"s":"A","l":""}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) - assertNotNull(item) - assertEquals(src.optString("s"), item?.s) - assertEquals(src.optLong("l"), item?.l) - } - run { - val src = """{"s":"A","l":100}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) - assertNotNull(item) - assertEquals(src.optString("s"), item?.s) - assertEquals(src.optLong("l"), item?.l) - } - run { - val src ="""{"s":"A","l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) - assertNotNull(item) - assertEquals(src.optString("s"), item?.s) - assertEquals(src.optLong("l"), item?.l) - } - } - - @Test - fun testParseList() { - assertEquals(0, parseList(::TestEntity, null).size) - - val src = JsonArray() - assertEquals(0, parseList(::TestEntity, src).size) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseList(::TestEntity, src).size) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseList(::TestEntity, src).size) - - // error - src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseList(::TestEntity, src).size) - - } - - @Test - fun testParseListOrNull() { - assertEquals(null, parseListOrNull(::TestEntity, null)) - - val src = JsonArray() - assertEquals(null, parseListOrNull(::TestEntity, src)) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseListOrNull(::TestEntity, src)?.size) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseListOrNull(::TestEntity, src)?.size) - - // error - src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseListOrNull(::TestEntity, src)?.size) - - } - - @Test - fun testParseMap() { - assertEquals(0, parseMap(::TestEntity, null).size) - - val src = JsonArray() - assertEquals(0, parseMap(::TestEntity, src).size) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseMap(::TestEntity, src).size) - - src.add("""{"s":"B","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseMap(::TestEntity, src).size) - - // error - src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseMap(::TestEntity, src).size) - - } - - @Test - fun testParseMapOrNull() { - assertEquals(null, parseMapOrNull(::TestEntity, null)) - - val src = JsonArray() - assertEquals(null, parseMapOrNull(::TestEntity, src)) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseMapOrNull(::TestEntity, src)?.size) - - src.add("""{"s":"B","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseMapOrNull(::TestEntity, src)?.size) - - // error - src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseMapOrNull(::TestEntity, src)?.size) - - } - - private val parser = TootParser(MockContext(), SavedAccount.na) - - @Test - fun testParseItemWithParser() { - - assertEquals(null, parseItem(::TestEntity, parser, null)) - - run { - val src ="""{"s":null,"l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) - assertNull(item) - } - run { - val src = """{"s":"","l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) - assertNull(item) - } - run { - val src = """{"s":"A","l":null}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) - assertNotNull(item) - assertEquals(src.optString("s"), item?.s) - assertEquals(src.optLong("l"), item?.l) - } - run { - val src = """{"s":"A","l":""}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) - assertNotNull(item) - assertEquals(src.optString("s"), item?.s) - assertEquals(src.optLong("l"), item?.l) - } - run { - val src = """{"s":"A","l":100}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) - assertNotNull(item) - assertEquals(src.optString("s"), item?.s) - assertEquals(src.optLong("l"), item?.l) - } - run { - val src = """{"s":"A","l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) - assertNotNull(item) - assertEquals(src.optString("s"), item?.s) - assertEquals(src.optLong("l"), item?.l) - } - } - - @Test - fun testParseListWithParser() { - assertEquals(0, parseList(::TestEntity, parser, null).size) - - val src = JsonArray() - assertEquals(0, parseList(::TestEntity, parser, src).size) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseList(::TestEntity, parser, src).size) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseList(::TestEntity, parser, src).size) - - // error - src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseList(::TestEntity, parser, src).size) - - } - - @Test - fun testParseListOrNullWithParser() { - assertEquals(null, parseListOrNull(::TestEntity, parser, null)) - - val src = JsonArray() - assertEquals(null, parseListOrNull(::TestEntity, parser, src)) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseListOrNull(::TestEntity, parser, src)?.size) - - src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseListOrNull(::TestEntity, parser, src)?.size) - - // error - src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseListOrNull(::TestEntity, parser, src)?.size) - - } - - @Test(expected = RuntimeException::class) - fun testNotEmptyOrThrow1() { - println(notEmptyOrThrow("param1", null)) - } - - @Test(expected = RuntimeException::class) - fun testNotEmptyOrThrow2() { - println(notEmptyOrThrow("param1", "")) - } - - @Test - fun testNotEmptyOrThrow3() { - assertEquals("A", notEmptyOrThrow("param1", "A")) - } - - @Test(expected = RuntimeException::class) - fun testNotEmptyOrThrow4() { - println("""{"param1":null}""".decodeJsonObject().stringOrThrow("param1")) - } - - @Test(expected = RuntimeException::class) - fun testNotEmptyOrThrow5() { - println("""{"param1":""}""".decodeJsonObject().stringOrThrow("param1")) - } - - @Test - fun testNotEmptyOrThrow6() { - assertEquals("A", """{"param1":"A"}""".decodeJsonObject().stringOrThrow("param1")) - } + + class TestEntity(val s: String, val l: Long) : Mappable { + constructor(src: JsonObject) : this( + s = src.stringOrThrow("s"), + l = src.long("l") ?: 0L + ) + + @Suppress("UNUSED_PARAMETER") + constructor(parser: TootParser, src: JsonObject) : this( + s = src.stringOrThrow("s"), + l = src.long("l") ?: 0L + ) + + override val mapKey: String + get() = s + } + + @Test + fun testParseItem() { + assertEquals(null, parseItem(::TestEntity, null)) + + run { + val src = """{"s":null,"l":"100"}""".decodeJsonObject() + val item = parseItem(::TestEntity, src) + assertNull(item) + } + run { + val src = """{"s":"","l":"100"}""".decodeJsonObject() + val item = parseItem(::TestEntity, src) + assertNull(item) + } + run { + val src = """{"s":"A","l":null}""".decodeJsonObject() + val item = parseItem(::TestEntity, src) + assertNotNull(item) + assertEquals(src.optString("s"), item?.s) + assertEquals(src.optLong("l"), item?.l) + } + run { + val src = """{"s":"A","l":""}""".decodeJsonObject() + val item = parseItem(::TestEntity, src) + assertNotNull(item) + assertEquals(src.optString("s"), item?.s) + assertEquals(src.optLong("l"), item?.l) + } + run { + val src = """{"s":"A","l":100}""".decodeJsonObject() + val item = parseItem(::TestEntity, src) + assertNotNull(item) + assertEquals(src.optString("s"), item?.s) + assertEquals(src.optLong("l"), item?.l) + } + run { + val src = """{"s":"A","l":"100"}""".decodeJsonObject() + val item = parseItem(::TestEntity, src) + assertNotNull(item) + assertEquals(src.optString("s"), item?.s) + assertEquals(src.optLong("l"), item?.l) + } + } + + @Test + fun testParseList() { + assertEquals(0, parseList(::TestEntity, null).size) + + val src = JsonArray() + assertEquals(0, parseList(::TestEntity, src).size) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(1, parseList(::TestEntity, src).size) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseList(::TestEntity, src).size) + + // error + src.add("""{"s":"","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseList(::TestEntity, src).size) + + } + + @Test + fun testParseListOrNull() { + assertEquals(null, parseListOrNull(::TestEntity, null)) + + val src = JsonArray() + assertEquals(null, parseListOrNull(::TestEntity, src)) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(1, parseListOrNull(::TestEntity, src)?.size) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseListOrNull(::TestEntity, src)?.size) + + // error + src.add("""{"s":"","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseListOrNull(::TestEntity, src)?.size) + + } + + @Test + fun testParseMap() { + assertEquals(0, parseMap(::TestEntity, null).size) + + val src = JsonArray() + assertEquals(0, parseMap(::TestEntity, src).size) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(1, parseMap(::TestEntity, src).size) + + src.add("""{"s":"B","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseMap(::TestEntity, src).size) + + // error + src.add("""{"s":"","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseMap(::TestEntity, src).size) + + } + + @Test + fun testParseMapOrNull() { + assertEquals(null, parseMapOrNull(::TestEntity, null)) + + val src = JsonArray() + assertEquals(null, parseMapOrNull(::TestEntity, src)) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(1, parseMapOrNull(::TestEntity, src)?.size) + + src.add("""{"s":"B","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseMapOrNull(::TestEntity, src)?.size) + + // error + src.add("""{"s":"","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseMapOrNull(::TestEntity, src)?.size) + + } + + private val parser = TootParser(MockContext(), SavedAccount.na) + + @Test + fun testParseItemWithParser() { + + assertEquals(null, parseItem(::TestEntity, parser, null)) + + run { + val src = """{"s":null,"l":"100"}""".decodeJsonObject() + val item = parseItem(::TestEntity, parser, src) + assertNull(item) + } + run { + val src = """{"s":"","l":"100"}""".decodeJsonObject() + val item = parseItem(::TestEntity, parser, src) + assertNull(item) + } + run { + val src = """{"s":"A","l":null}""".decodeJsonObject() + val item = parseItem(::TestEntity, parser, src) + assertNotNull(item) + assertEquals(src.optString("s"), item?.s) + assertEquals(src.optLong("l"), item?.l) + } + run { + val src = """{"s":"A","l":""}""".decodeJsonObject() + val item = parseItem(::TestEntity, parser, src) + assertNotNull(item) + assertEquals(src.optString("s"), item?.s) + assertEquals(src.optLong("l"), item?.l) + } + run { + val src = """{"s":"A","l":100}""".decodeJsonObject() + val item = parseItem(::TestEntity, parser, src) + assertNotNull(item) + assertEquals(src.optString("s"), item?.s) + assertEquals(src.optLong("l"), item?.l) + } + run { + val src = """{"s":"A","l":"100"}""".decodeJsonObject() + val item = parseItem(::TestEntity, parser, src) + assertNotNull(item) + assertEquals(src.optString("s"), item?.s) + assertEquals(src.optLong("l"), item?.l) + } + } + + @Test + fun testParseListWithParser() { + assertEquals(0, parseList(::TestEntity, parser, null).size) + + val src = JsonArray() + assertEquals(0, parseList(::TestEntity, parser, src).size) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(1, parseList(::TestEntity, parser, src).size) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseList(::TestEntity, parser, src).size) + + // error + src.add("""{"s":"","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseList(::TestEntity, parser, src).size) + + } + + @Test + fun testParseListOrNullWithParser() { + assertEquals(null, parseListOrNull(::TestEntity, parser, null)) + + val src = JsonArray() + assertEquals(null, parseListOrNull(::TestEntity, parser, src)) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(1, parseListOrNull(::TestEntity, parser, src)?.size) + + src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseListOrNull(::TestEntity, parser, src)?.size) + + // error + src.add("""{"s":"","l":"100"}""".decodeJsonObject()) + assertEquals(2, parseListOrNull(::TestEntity, parser, src)?.size) + + } + + @Test(expected = RuntimeException::class) + fun testNotEmptyOrThrow1() { + println(notEmptyOrThrow("param1", null)) + } + + @Test(expected = RuntimeException::class) + fun testNotEmptyOrThrow2() { + println(notEmptyOrThrow("param1", "")) + } + + @Test + fun testNotEmptyOrThrow3() { + assertEquals("A", notEmptyOrThrow("param1", "A")) + } + + @Test(expected = RuntimeException::class) + fun testNotEmptyOrThrow4() { + println("""{"param1":null}""".decodeJsonObject().stringOrThrow("param1")) + } + + @Test(expected = RuntimeException::class) + fun testNotEmptyOrThrow5() { + println("""{"param1":""}""".decodeJsonObject().stringOrThrow("param1")) + } + + @Test + fun testNotEmptyOrThrow6() { + assertEquals("A", """{"param1":"A"}""".decodeJsonObject().stringOrThrow("param1")) + } } \ No newline at end of file diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestTootAccount.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestTootAccount.kt index 295fb1c4..44cb0c46 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestTootAccount.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestTootAccount.kt @@ -2,60 +2,68 @@ package jp.juggler.subwaytooter.api.entity import androidx.test.runner.AndroidJUnit4 import jp.juggler.subwaytooter.util.LinkHelper - +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.assertEquals - @RunWith(AndroidJUnit4::class) class TestTootAccount { - - @Test - @Throws(Exception::class) - fun testFindHostFromUrl() { - - // all null - assertEquals(null, TootAccount.findHostFromUrl(null, null, null).first) - - // find from acct - assertEquals(null, TootAccount.findHostFromUrl("", null, null).first) - assertEquals(null, TootAccount.findHostFromUrl("user", null, null).first) - assertEquals(null, TootAccount.findHostFromUrl("user@", null, null).first) - assertEquals("host", TootAccount.findHostFromUrl("user@HOST", null, null).first) - - // find from accessHost - assertEquals( - "", - TootAccount.findHostFromUrl(null, LinkHelper.create(Host.parse("")), null).first - ) - assertEquals( - "any string is allowed", - TootAccount.findHostFromUrl( - null, - LinkHelper.create(Host.parse("any string is allowed")), - null - ).first - ) - - // find from url - assertEquals(null, TootAccount.findHostFromUrl(null, null, "").first) - assertEquals(null, TootAccount.findHostFromUrl(null, null, "xxx").first) - assertEquals( - null, - TootAccount.findHostFromUrl(null, null, "mailto:tateisu@gmail.com").first - ) - assertEquals( - "mastodon.juggler.jp", - TootAccount.findHostFromUrl(null, null, "https://MASTODON.juggler.jp/@tateisu").first - ) - assertEquals( - "mastodon.juggler.jp", - TootAccount.findHostFromUrl(null, null, "https://mastodon.juggler.jp/").first - ) - assertEquals( - "mastodon.juggler.jp", - TootAccount.findHostFromUrl(null, null, "https://mastodon.juggler.jp").first - ) - } + + @Test + @Throws(Exception::class) + fun testFindHostFromUrl() { + + val emptyHost = Host.EMPTY + + // all null + var pair = TootAccount.findHostFromUrl(null, null, null) + assertEquals(null, pair.first) + + // find from acct + pair = TootAccount.findHostFromUrl("", null, null) + assertEquals(Host.UNKNOWN, pair.first) + assertEquals(null, TootAccount.findHostFromUrl("user", null, null).first) + assertEquals(emptyHost, TootAccount.findHostFromUrl("user@", null, null).first) + assertEquals( + "host", + TootAccount.findHostFromUrl("user@HOST", null, null) + .first?.ascii + ) + + // find from accessHost + + assertEquals( + emptyHost, + TootAccount.findHostFromUrl(null, LinkHelper.create(emptyHost), null).first + ) + val testHost = Host.parse("any string is allowed") + assertEquals( + testHost, + TootAccount.findHostFromUrl( + null, + LinkHelper.create(testHost), + null + ).first + ) + + // find from url + assertEquals(null, TootAccount.findHostFromUrl(null, null, "").first) + assertEquals(null, TootAccount.findHostFromUrl(null, null, "xxx").first) + assertEquals( + null, + TootAccount.findHostFromUrl(null, null, "mailto:tateisu@gmail.com").first + ) + assertEquals( + Host.parse("mastodon.juggler.jp"), + TootAccount.findHostFromUrl(null, null, "https://MASTODON.juggler.jp/@tateisu").first + ) + assertEquals( + Host.parse("mastodon.juggler.jp"), + TootAccount.findHostFromUrl(null, null, "https://mastodon.juggler.jp/").first + ) + assertEquals( + Host.parse("mastodon.juggler.jp"), + TootAccount.findHostFromUrl(null, null, "https://mastodon.juggler.jp").first + ) + } } diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestArrayListSizeBug.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestArrayListSizeBug.kt new file mode 100644 index 00000000..9f8281bb --- /dev/null +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestArrayListSizeBug.kt @@ -0,0 +1,26 @@ +package jp.juggler.subwaytooter.database + +import androidx.test.runner.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TestArrayListSizeBug { + + @Test + fun testArrayListSize() { + val list = ArrayList(arrayOf("c", "b", "a").toList()) + assertEquals("ArrayList size", 3, list.size) + } + + class ArrayListDerived(args: List) : ArrayList(args) + + @Test + fun testArrayListDerived() { + val list = ArrayListDerived(arrayOf("c", "b", "a").toList()) + assertEquals("ArrayListDerived size", 3, list.size) + // kotlin 1.5.31で(Javaの) size() ではなく getSize() にアクセスしようとして例外を出していた + // kotlin 1.5.30では大丈夫だったが、JetPack Composeは 1.5.31を要求するのだった…。 + } +} diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt new file mode 100644 index 00000000..4cc41007 --- /dev/null +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt @@ -0,0 +1,66 @@ +package jp.juggler.subwaytooter.database + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 +import jp.juggler.subwaytooter.App1 +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TestDatabase { + + private class MockDbHelper( + context: Context, + dbName: String, + dbVersion: Int, + val create: (SQLiteDatabase) -> Unit, + val upgrade: (SQLiteDatabase, Int, Int) -> Unit, + ) : SQLiteOpenHelper(context, dbName, null, dbVersion) { + override fun onCreate(db: SQLiteDatabase) { + create(db) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + upgrade(db, oldVersion, newVersion) + } + } + + @Test + fun testCreateAll() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val dbName = "testCreateAll" + val helper = MockDbHelper( + context, + dbName, + App1.DB_VERSION, + create = { db -> + App1.tableList.forEach { + val ex = try { + it.onDBCreate(db) + null + } catch (ex: Throwable) { + ex + } + assertNull("${it.table} onDBCreate", ex) + } + }, + upgrade = { db, oldV, newV -> + App1.tableList.forEach { + val ex = try { + it.onDBUpgrade(db, oldV, newV) + null + } catch (ex: Throwable) { + ex + } + assertNull("${it.table} onDBUpgrade", ex) + } + } + ) + context.deleteDatabase(dbName) + helper.writableDatabase + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestBucketList.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestBucketList.kt index f2db32d3..0576d13b 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestBucketList.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestBucketList.kt @@ -1,24 +1,25 @@ package jp.juggler.subwaytooter.util -import androidx.test.runner.AndroidJUnit4 -import org.junit.Assert.* +import androidx.test.runner.AndroidJUnit4 +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @Suppress("MemberVisibilityCanPrivate") @RunWith(AndroidJUnit4::class) class TestBucketList { - @Test fun test1(){ - val list = BucketList(bucketCapacity=2) - assertEquals(true,list.isEmpty()) - list.addAll( listOf("A","B","C")) - list.addAll( 3, listOf("a","b","c")) - list.addAll( 1, listOf("a","b","c")) - list.removeAt(7) - assertEquals(8,list.size) - listOf("A","a","b","c","B","C","a","c").forEachIndexed { i,v-> - assertEquals( v,list[i]) - } - } + @Test + fun test1() { + val list = BucketList(bucketCapacity = 2) + assertEquals(true, list.isEmpty()) + list.addAll(listOf("A", "B", "C")) + list.addAll(3, listOf("a", "b", "c")) + list.addAll(1, listOf("a", "b", "c")) + list.removeAt(7) + assertEquals(8, list.size) + listOf("A", "a", "b", "c", "B", "C", "a", "c").forEachIndexed { i, v -> + assertEquals(v, list[i]) + } + } } \ No newline at end of file diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestHtmlDecoder.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestHtmlDecoder.kt index a28baf1a..f698aca5 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestHtmlDecoder.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestHtmlDecoder.kt @@ -1,6 +1,6 @@ package jp.juggler.subwaytooter.util -import androidx.test.InstrumentationRegistry +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.util.neatSpaces @@ -12,46 +12,47 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TestHtmlDecoder { - class SpanMeta( - val span:Any, - val start:Int, - val end:Int, - val flags:Int, - val text :String - ){ - override fun toString() = "[$start..$end) $flags ${span.javaClass.simpleName} $text" - } - - @Test fun test1(){ - // Context of the app under test. - val appContext = InstrumentationRegistry.getTargetContext() - - val options = DecodeOptions(appContext,LinkHelper.create(Host.parse("instance.test"))) - - val html = """ + class SpanMeta( + val span: Any, + val start: Int, + val end: Int, + val flags: Int, + val text: String + ) { + override fun toString() = "[$start..$end) $flags ${span.javaClass.simpleName} $text" + } + + @Test + fun test1() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + + val options = DecodeOptions(appContext, LinkHelper.create(Host.parse("instance.test"))) + + val html = """ 日本語で楽しめるMastodonサーバを提供しています。 利用規約を読んでからサインアップしてください。 Androidアプリ iOSアプリ long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text """.trimIndent() - val text = options.decodeHTML( html ).neatSpaces() - val spanArray = text.getSpans(0,text.length,Any::class.java).map { - val start = text.getSpanStart(it) - val end = text.getSpanEnd(it) - SpanMeta( - span = it, - start = start, - end = end, - flags = text.getSpanFlags(it), - text = text.subSequence(start, end).toString() - ) - } - - spanArray.forEach{ println(it)} - assertEquals(3,spanArray.size) - assertEquals( "利用規約", spanArray[0].text) - assertEquals( "", spanArray[1].text) - assertEquals( "", spanArray[2].text) - } + val text = options.decodeHTML(html).neatSpaces() + val spanArray = text.getSpans(0, text.length, Any::class.java).map { + val start = text.getSpanStart(it) + val end = text.getSpanEnd(it) + SpanMeta( + span = it, + start = start, + end = end, + flags = text.getSpanFlags(it), + text = text.subSequence(start, end).toString() + ) + } + + spanArray.forEach { println(it) } + assertEquals(5, spanArray.size) + assertEquals("利用規約", spanArray[0].text) + assertEquals("Androidアプリ", spanArray[1].text) + assertEquals("", spanArray[2].text) + } } \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.kt b/app/src/main/java/jp/juggler/subwaytooter/App1.kt index 9165bff9..1adbf285 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.kt @@ -131,9 +131,9 @@ class App1 : Application() { // 2021/5/11 59=>60 SavedAccountテーブルに項目追加 // 2021/5/23 60=>61 SavedAccountテーブルに項目追加 - internal const val DB_VERSION = 61 + const val DB_VERSION = 61 - private val tableList = arrayOf( + val tableList = arrayOf( LogData, SavedAccount, ClientInfo, diff --git a/app/src/main/java/jp/juggler/subwaytooter/Pref.kt b/app/src/main/java/jp/juggler/subwaytooter/Pref.kt index 22aab491..d81eb08c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Pref.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/Pref.kt @@ -3,12 +3,11 @@ package jp.juggler.subwaytooter import android.content.Context import android.content.SharedPreferences import android.graphics.Color -import androidx.preference.PreferenceManager import jp.juggler.subwaytooter.itemviewholder.AdditionalButtonsPosition import jp.juggler.util.optInt fun Context.pref(): SharedPreferences = - PreferenceManager.getDefaultSharedPreferences(this) + this.getSharedPreferences(this.packageName + "_preferences", Context.MODE_PRIVATE) @Suppress("EqualsOrHashCode") abstract class BasePref(val key: String, val defVal: T) { @@ -503,7 +502,8 @@ object PrefI { const val VS_MISSKEY = 2 val ipVisibilityStyle = IntPref("ipVisibilityStyle", VS_BY_ACCOUNT) - val ipAdditionalButtonsPosition = IntPref("AdditionalButtonsPosition", AdditionalButtonsPosition.End.idx) + val ipAdditionalButtonsPosition = + IntPref("AdditionalButtonsPosition", AdditionalButtonsPosition.End.idx) val ipFooterButtonBgColor = IntPref("footer_button_bg_color", 0) val ipFooterButtonFgColor = IntPref("footer_button_fg_color", 0) diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.kt b/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.kt index 7156c0b1..9580f3f2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.kt @@ -75,7 +75,7 @@ class AcctColor { private val log = LogCategory("AcctColor") - const val table = "acct_color" + override val table = "acct_color" val columnList: ColumnMeta.List = ColumnMeta.List(table, 9).apply { // not used, but must be defined diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/AcctSet.kt b/app/src/main/java/jp/juggler/subwaytooter/table/AcctSet.kt index cae84d2d..2e5aa793 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/AcctSet.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/AcctSet.kt @@ -16,7 +16,7 @@ object AcctSet : TableCompanion { private val log = LogCategory("AcctSet") - private const val table = "acct_set" + override val table = "acct_set" val columnList: ColumnMeta.List = ColumnMeta.List(table, 7).apply { ColumnMeta(this, 0, BaseColumns._ID, "INTEGER PRIMARY KEY", primary = true) diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.kt b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.kt index 564f36d1..6199d52c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.kt @@ -9,7 +9,7 @@ import jp.juggler.util.* object ClientInfo : TableCompanion { private val log = LogCategory("ClientInfo") - const val table = "client_info2" + override val table = "client_info2" val columnList: ColumnMeta.List = ColumnMeta.List(table, 19).apply { ColumnMeta(this, 0, BaseColumns._ID, "INTEGER PRIMARY KEY", primary = true) createExtra = { diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarning.kt b/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarning.kt index ff40c754..18634a58 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarning.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarning.kt @@ -11,7 +11,7 @@ import jp.juggler.util.* object ContentWarning : TableCompanion { private val log = LogCategory("ContentWarning") - private const val table = "content_warning" + override val table = "content_warning" val columnList: ColumnMeta.List = ColumnMeta.List(table, 0).apply { ColumnMeta(this, 0, BaseColumns._ID, "INTEGER PRIMARY KEY", primary = true) diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/FavMute.kt b/app/src/main/java/jp/juggler/subwaytooter/table/FavMute.kt index 20bf1ec1..4f6153fa 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/FavMute.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/FavMute.kt @@ -13,7 +13,7 @@ object FavMute : TableCompanion { private val log = LogCategory("FavMute") - const val table = "fav_mute" + override val table = "fav_mute" const val COL_ID = "_id" const val COL_ACCT = "acct" diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/HighlightWord.kt b/app/src/main/java/jp/juggler/subwaytooter/table/HighlightWord.kt index 6e3169bd..5a03b29b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/HighlightWord.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/HighlightWord.kt @@ -19,7 +19,7 @@ class HighlightWord { const val SOUND_TYPE_DEFAULT = 1 const val SOUND_TYPE_CUSTOM = 2 - const val table = "highlight_word" + override val table = "highlight_word" const val COL_ID = BaseColumns._ID const val COL_NAME = "name" private const val COL_TIME_SAVE = "time_save" diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/LogData.kt b/app/src/main/java/jp/juggler/subwaytooter/table/LogData.kt index f6faa3c2..8fe7689a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/LogData.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/LogData.kt @@ -6,7 +6,7 @@ import jp.juggler.util.TableCompanion object LogData : TableCompanion { // private const val TAG = "SubwayTooter" - internal const val table = "warning" + override val table = "warning" private const val COL_TIME = "t" private const val COL_LEVEL = "l" diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/MediaShown.kt b/app/src/main/java/jp/juggler/subwaytooter/table/MediaShown.kt index 865d4bae..9cf9b22d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/MediaShown.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/MediaShown.kt @@ -10,7 +10,7 @@ import jp.juggler.util.* object MediaShown : TableCompanion { private val log = LogCategory("MediaShown") - private const val table = "media_shown_misskey" + override val table = "media_shown_misskey" val columnList: ColumnMeta.List = ColumnMeta.List(table, 30).apply { ColumnMeta(this, 0, BaseColumns._ID, "INTEGER PRIMARY KEY", primary = true) diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/MutedApp.kt b/app/src/main/java/jp/juggler/subwaytooter/table/MutedApp.kt index 28f2e0ad..444b1b65 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/MutedApp.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/MutedApp.kt @@ -14,7 +14,7 @@ object MutedApp : TableCompanion { private val log = LogCategory("MutedApp") - internal const val table = "app_mute" + override val table = "app_mute" const val COL_ID = "_id" const val COL_NAME = "name" private const val COL_TIME_SAVE = "time_save" diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.kt b/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.kt index 1f3dfec0..e6923705 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.kt @@ -13,7 +13,7 @@ object MutedWord : TableCompanion { private val log = LogCategory("MutedWord") - const val table = "word_mute" + override val table = "word_mute" const val COL_ID = "_id" const val COL_NAME = "name" private const val COL_TIME_SAVE = "time_save" diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationCache.kt b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationCache.kt index f5b1a67d..eba15e75 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationCache.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationCache.kt @@ -28,7 +28,7 @@ class NotificationCache(private val account_db_id: Long) { private val log = LogCategory("NotificationCache") - private const val table = "noti_cache" + override val table = "noti_cache" private const val COL_ID = BaseColumns._ID diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.kt b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.kt index 301e1351..efec0ff4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.kt @@ -83,7 +83,12 @@ class NotificationTracking { val cv = ContentValues() postId.putTo(cv, COL_POST_ID) cv.put(COL_POST_TIME, postTime) - val rows = App1.database.update(table, cv, WHERE_AID, arrayOf(accountDbId.toString(), notificationType)) + val rows = App1.database.update( + table, + cv, + WHERE_AID, + arrayOf(accountDbId.toString(), notificationType) + ) log.d("updatePost account_db_id=$accountDbId, nt=$notificationType, post=$postId,$postTime update_rows=$rows") dirty = false clearCache(accountDbId, notificationType) @@ -97,7 +102,7 @@ class NotificationTracking { private val log = LogCategory("NotificationTracking") - private const val table = "noti_trac" + override val table = "noti_trac" private const val COL_ID = BaseColumns._ID @@ -179,9 +184,13 @@ class NotificationTracking { ///////////////////////////////////////////////////////////////////////////////// - private val cache = ConcurrentHashMap>() + private val cache = + ConcurrentHashMap>() - private fun ConcurrentHashMap.getOrCreate(key: K, creator: () -> V): V { + private fun ConcurrentHashMap.getOrCreate( + key: K, + creator: () -> V + ): V { var v = this[key] if (v == null) v = creator().also { this[key] = it } return v @@ -193,7 +202,11 @@ class NotificationTracking { private fun clearCache(accountDbId: Long, notificationType: String): NotificationTracking? = cache[accountDbId]?.remove(notificationType) - private fun saveCache(accountDbId: Long, notificationType: String, nt: NotificationTracking) { + private fun saveCache( + accountDbId: Long, + notificationType: String, + nt: NotificationTracking + ) { cache.getOrCreate(accountDbId) { ConcurrentHashMap() }[notificationType] = nt @@ -248,7 +261,8 @@ class NotificationTracking { log.i("$acct/$notificationType read>show! clip to $show") val cv = ContentValues() show.putTo(cv, COL_NID_READ) //変数名とキー名が異なるのに注意 - val where_args = arrayOf(accountDbId.toString(), notificationType) + val where_args = + arrayOf(accountDbId.toString(), notificationType) App1.database.update(table, cv, WHERE_AID, where_args) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/PostDraft.kt b/app/src/main/java/jp/juggler/subwaytooter/table/PostDraft.kt index 20b66406..481ab859 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/PostDraft.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/PostDraft.kt @@ -34,7 +34,7 @@ class PostDraft { private val log = LogCategory("PostDraft") - private const val table = "post_draft" + override val table = "post_draft" private const val COL_ID = BaseColumns._ID private const val COL_TIME_SAVE = "time_save" private const val COL_JSON = "json" diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt index 30e8a72e..bd363079 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt @@ -344,7 +344,7 @@ class SavedAccount( private val log = LogCategory("SavedAccount") - const val table = "access_info" + override val table = "access_info" val columnList = ColumnMeta.List(table, 0).apply { createExtra = { @@ -355,7 +355,8 @@ class SavedAccount( } } - private val COL_ID = ColumnMeta(columnList, 0, BaseColumns._ID, "INTEGER PRIMARY KEY", primary = true) + private val COL_ID = + ColumnMeta(columnList, 0, BaseColumns._ID, "INTEGER PRIMARY KEY", primary = true) private val COL_HOST = ColumnMeta(columnList, 0, "h", "text not null") private val COL_DOMAIN = ColumnMeta(columnList, 56, "d", "text") private val COL_USER = ColumnMeta(columnList, 0, "u", "text not null") @@ -363,30 +364,48 @@ class SavedAccount( private val COL_TOKEN = ColumnMeta(columnList, 0, "t", "text not null") private val COL_VISIBILITY = ColumnMeta(columnList, 0, "visibility", "text") - private val COL_CONFIRM_BOOST = ColumnMeta(columnList, 0, "confirm_boost", ColumnMeta.TS_TRUE) - private val COL_DONT_HIDE_NSFW = ColumnMeta(columnList, 0, "dont_hide_nsfw", ColumnMeta.TS_ZERO) + private val COL_CONFIRM_BOOST = + ColumnMeta(columnList, 0, "confirm_boost", ColumnMeta.TS_TRUE) + private val COL_DONT_HIDE_NSFW = + ColumnMeta(columnList, 0, "dont_hide_nsfw", ColumnMeta.TS_ZERO) - private val COL_NOTIFICATION_MENTION = ColumnMeta(columnList, 2, "notification_mention", ColumnMeta.TS_TRUE) - private val COL_NOTIFICATION_BOOST = ColumnMeta(columnList, 2, "notification_boost", ColumnMeta.TS_TRUE) - private val COL_NOTIFICATION_FAVOURITE = ColumnMeta(columnList, 2, "notification_favourite", ColumnMeta.TS_TRUE) - private val COL_NOTIFICATION_FOLLOW = ColumnMeta(columnList, 2, "notification_follow", ColumnMeta.TS_TRUE) + private val COL_NOTIFICATION_MENTION = + ColumnMeta(columnList, 2, "notification_mention", ColumnMeta.TS_TRUE) + private val COL_NOTIFICATION_BOOST = + ColumnMeta(columnList, 2, "notification_boost", ColumnMeta.TS_TRUE) + private val COL_NOTIFICATION_FAVOURITE = + ColumnMeta(columnList, 2, "notification_favourite", ColumnMeta.TS_TRUE) + private val COL_NOTIFICATION_FOLLOW = + ColumnMeta(columnList, 2, "notification_follow", ColumnMeta.TS_TRUE) private val COL_NOTIFICATION_FOLLOW_REQUEST = ColumnMeta(columnList, 44, "notification_follow_request", ColumnMeta.TS_TRUE) - private val COL_NOTIFICATION_REACTION = ColumnMeta(columnList, 33, "notification_reaction", ColumnMeta.TS_TRUE) - private val COL_NOTIFICATION_VOTE = ColumnMeta(columnList, 33, "notification_vote", ColumnMeta.TS_TRUE) - private val COL_NOTIFICATION_POST = ColumnMeta(columnList, 57, "notification_post", ColumnMeta.TS_TRUE) + private val COL_NOTIFICATION_REACTION = + ColumnMeta(columnList, 33, "notification_reaction", ColumnMeta.TS_TRUE) + private val COL_NOTIFICATION_VOTE = + ColumnMeta(columnList, 33, "notification_vote", ColumnMeta.TS_TRUE) + private val COL_NOTIFICATION_POST = + ColumnMeta(columnList, 57, "notification_post", ColumnMeta.TS_TRUE) - private val COL_CONFIRM_FOLLOW = ColumnMeta(columnList, 10, "confirm_follow", ColumnMeta.TS_TRUE) - private val COL_CONFIRM_FOLLOW_LOCKED = ColumnMeta(columnList, 10, "confirm_follow_locked", ColumnMeta.TS_TRUE) - private val COL_CONFIRM_UNFOLLOW = ColumnMeta(columnList, 10, "confirm_unfollow", ColumnMeta.TS_TRUE) - private val COL_CONFIRM_POST = ColumnMeta(columnList, 10, "confirm_post", ColumnMeta.TS_TRUE) - private val COL_CONFIRM_FAVOURITE = ColumnMeta(columnList, 23, "confirm_favourite", ColumnMeta.TS_TRUE) - private val COL_CONFIRM_UNBOOST = ColumnMeta(columnList, 24, "confirm_unboost", ColumnMeta.TS_TRUE) - private val COL_CONFIRM_UNFAVOURITE = ColumnMeta(columnList, 24, "confirm_unfavourite", ColumnMeta.TS_TRUE) - private val COL_CONFIRM_REACTION = ColumnMeta(columnList, 61, "confirm_reaction", ColumnMeta.TS_TRUE) + private val COL_CONFIRM_FOLLOW = + ColumnMeta(columnList, 10, "confirm_follow", ColumnMeta.TS_TRUE) + private val COL_CONFIRM_FOLLOW_LOCKED = + ColumnMeta(columnList, 10, "confirm_follow_locked", ColumnMeta.TS_TRUE) + private val COL_CONFIRM_UNFOLLOW = + ColumnMeta(columnList, 10, "confirm_unfollow", ColumnMeta.TS_TRUE) + private val COL_CONFIRM_POST = + ColumnMeta(columnList, 10, "confirm_post", ColumnMeta.TS_TRUE) + private val COL_CONFIRM_FAVOURITE = + ColumnMeta(columnList, 23, "confirm_favourite", ColumnMeta.TS_TRUE) + private val COL_CONFIRM_UNBOOST = + ColumnMeta(columnList, 24, "confirm_unboost", ColumnMeta.TS_TRUE) + private val COL_CONFIRM_UNFAVOURITE = + ColumnMeta(columnList, 24, "confirm_unfavourite", ColumnMeta.TS_TRUE) + private val COL_CONFIRM_REACTION = + ColumnMeta(columnList, 61, "confirm_reaction", ColumnMeta.TS_TRUE) // スキーマ13から - val COL_NOTIFICATION_TAG = ColumnMeta(columnList, 13, "notification_server", ColumnMeta.TS_EMPTY) + val COL_NOTIFICATION_TAG = + ColumnMeta(columnList, 13, "notification_server", ColumnMeta.TS_EMPTY) // スキーマ14から val COL_REGISTER_KEY = ColumnMeta(columnList, 14, "register_key", ColumnMeta.TS_EMPTY) @@ -396,29 +415,40 @@ class SavedAccount( private val COL_SOUND_URI = ColumnMeta(columnList, 16, "sound_uri", ColumnMeta.TS_EMPTY) // スキーマ18から - private val COL_DONT_SHOW_TIMEOUT = ColumnMeta(columnList, 18, "dont_show_timeout", ColumnMeta.TS_ZERO) + private val COL_DONT_SHOW_TIMEOUT = + ColumnMeta(columnList, 18, "dont_show_timeout", ColumnMeta.TS_ZERO) // スキーマ27から - private val COL_DEFAULT_TEXT = ColumnMeta(columnList, 27, "default_text", ColumnMeta.TS_EMPTY) + private val COL_DEFAULT_TEXT = + ColumnMeta(columnList, 27, "default_text", ColumnMeta.TS_EMPTY) // スキーマ28から - private val COL_MISSKEY_VERSION = ColumnMeta(columnList, 28, "is_misskey", ColumnMeta.TS_ZERO) + private val COL_MISSKEY_VERSION = + ColumnMeta(columnList, 28, "is_misskey", ColumnMeta.TS_ZERO) // カラム名がおかしいのは、昔はboolean扱いだったから // 0: not misskey // 1: old(v10) misskey // 11: misskey v11 - private val COL_DEFAULT_SENSITIVE = ColumnMeta(columnList, 38, "default_sensitive", ColumnMeta.TS_ZERO) + private val COL_DEFAULT_SENSITIVE = + ColumnMeta(columnList, 38, "default_sensitive", ColumnMeta.TS_ZERO) private val COL_EXPAND_CW = ColumnMeta(columnList, 38, "expand_cw", ColumnMeta.TS_ZERO) - private val COL_MAX_TOOT_CHARS = ColumnMeta(columnList, 39, "max_toot_chars", ColumnMeta.TS_ZERO) + private val COL_MAX_TOOT_CHARS = + ColumnMeta(columnList, 39, "max_toot_chars", ColumnMeta.TS_ZERO) - private val COL_LAST_NOTIFICATION_ERROR = ColumnMeta(columnList, 42, "last_notification_error", "text") - private val COL_LAST_SUBSCRIPTION_ERROR = ColumnMeta(columnList, 45, "last_subscription_error", "text") - private val COL_LAST_PUSH_ENDPOINT = ColumnMeta(columnList, 46, "last_push_endpoint", "text") + private val COL_LAST_NOTIFICATION_ERROR = + ColumnMeta(columnList, 42, "last_notification_error", "text") + private val COL_LAST_SUBSCRIPTION_ERROR = + ColumnMeta(columnList, 45, "last_subscription_error", "text") + private val COL_LAST_PUSH_ENDPOINT = + ColumnMeta(columnList, 46, "last_push_endpoint", "text") - private val COL_IMAGE_RESIZE = ColumnMeta(columnList, 59, "image_resize", "text default null") - private val COL_IMAGE_MAX_MEGABYTES = ColumnMeta(columnList, 59, "image_max_megabytes", "text default null") - private val COL_MOVIE_MAX_MEGABYTES = ColumnMeta(columnList, 59, "movie_max_megabytes", "text default null") + private val COL_IMAGE_RESIZE = + ColumnMeta(columnList, 59, "image_resize", "text default null") + private val COL_IMAGE_MAX_MEGABYTES = + ColumnMeta(columnList, 59, "image_max_megabytes", "text default null") + private val COL_MOVIE_MAX_MEGABYTES = + ColumnMeta(columnList, 59, "movie_max_megabytes", "text default null") private val COL_PUSH_POLICY = ColumnMeta(columnList, 60, "push_policy", "text default null") @@ -563,7 +593,10 @@ class SavedAccount( } catch (ex: Throwable) { log.trace(ex) log.e(ex, "loadAccountList failed.") - context.showToast(true, ex.withCaption("(SubwayTooter) broken in-app database?")) + context.showToast( + true, + ex.withCaption("(SubwayTooter) broken in-app database?") + ) } } @@ -827,7 +860,14 @@ class SavedAccount( this.loginAccount = ta ContentValues().apply { put(COL_ACCOUNT, result.jsonObject.toString()) - }.let { App1.database.update(table, it, "$COL_ID=?", arrayOf(db_id.toString())) } + }.let { + App1.database.update( + table, + it, + "$COL_ID=?", + arrayOf(db_id.toString()) + ) + } PollingWorker.queueUpdateNotification(context) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/SubscriptionServerKey.kt b/app/src/main/java/jp/juggler/subwaytooter/table/SubscriptionServerKey.kt index b904bb3a..cdfb11c3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/SubscriptionServerKey.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/SubscriptionServerKey.kt @@ -13,7 +13,7 @@ object SubscriptionServerKey : TableCompanion { private val log = LogCategory("ServerKey") - private const val table = "subscription_server_key2" + override val table = "subscription_server_key2" private const val COL_ID = BaseColumns._ID private const val COL_CLIENT_IDENTIFIER = "ci" private const val COL_SERVER_KEY = "sk" diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/TagSet.kt b/app/src/main/java/jp/juggler/subwaytooter/table/TagSet.kt index 86542b34..2bd70efb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/TagSet.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/TagSet.kt @@ -2,18 +2,16 @@ package jp.juggler.subwaytooter.table import android.content.ContentValues import android.database.sqlite.SQLiteDatabase - -import java.util.ArrayList - import jp.juggler.subwaytooter.App1 import jp.juggler.util.LogCategory import jp.juggler.util.TableCompanion +import java.util.* object TagSet : TableCompanion { private val log = LogCategory("TagSet") - private const val table = "tag_set" + override val table = "tag_set" private const val COL_TIME_SAVE = "time_save" private const val COL_TAG = "tag" // タグ。先頭の#を含まない diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/UserRelation.kt b/app/src/main/java/jp/juggler/subwaytooter/table/UserRelation.kt index c691f63f..6bc09fbf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/UserRelation.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/UserRelation.kt @@ -48,7 +48,7 @@ class UserRelation { private val log = LogCategory("UserRelationMisskey") - private const val table = "user_relation_misskey" + override val table = "user_relation_misskey" val columnList: ColumnMeta.List = ColumnMeta.List(table, 30).apply { createExtra = { @@ -60,7 +60,8 @@ class UserRelation { deleteBeforeCreate = true } - val COL_ID = ColumnMeta(columnList, 0, BaseColumns._ID, "INTEGER PRIMARY KEY", primary = true) + val COL_ID = + ColumnMeta(columnList, 0, BaseColumns._ID, "INTEGER PRIMARY KEY", primary = true) private val COL_TIME_SAVE = ColumnMeta(columnList, 0, "time_save", "integer not null") // SavedAccount のDB_ID。 疑似アカウント用のエントリは -2L @@ -73,10 +74,12 @@ class UserRelation { private val COL_BLOCKING = ColumnMeta(columnList, 0, "blocking", "integer not null") private val COL_MUTING = ColumnMeta(columnList, 0, "muting", "integer not null") private val COL_REQUESTED = ColumnMeta(columnList, 0, "requested", "integer not null") - private val COL_FOLLOWING_REBLOGS = ColumnMeta(columnList, 0, "following_reblogs", "integer not null") + private val COL_FOLLOWING_REBLOGS = + ColumnMeta(columnList, 0, "following_reblogs", "integer not null") private val COL_ENDORSED = ColumnMeta(columnList, 32, "endorsed", "integer default 0") private val COL_BLOCKED_BY = ColumnMeta(columnList, 34, "blocked_by", "integer default 0") - private val COL_REQUESTED_BY = ColumnMeta(columnList, 35, "requested_by", "integer default 0") + private val COL_REQUESTED_BY = + ColumnMeta(columnList, 35, "requested_by", "integer default 0") private val COL_NOTE = ColumnMeta(columnList, 55, "note", "text default null") private val COL_NOTIFYING = ColumnMeta(columnList, 58, "notifying", "integer default 0") diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt index 54f572e5..5658a880 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt @@ -17,6 +17,8 @@ import jp.juggler.apng.ApngFrames import jp.juggler.subwaytooter.App1 import jp.juggler.util.LogCategory import jp.juggler.util.TableCompanion +import jp.juggler.util.getBlobOrNull +import jp.juggler.util.getLong import kotlinx.coroutines.channels.Channel import java.io.ByteArrayInputStream import java.lang.ref.WeakReference @@ -55,7 +57,7 @@ class CustomEmojiCache( companion object : TableCompanion { - const val table = "custom_emoji_cache" + override val table = "custom_emoji_cache" const val COL_ID = BaseColumns._ID const val COL_TIME_SAVE = "time_save" @@ -91,9 +93,9 @@ class CustomEmojiCache( )?.use { cursor -> if (cursor.moveToNext()) { DbCache( - id = cursor.getLong(cursor.getColumnIndex(COL_ID)), - timeUsed = cursor.getLong(cursor.getColumnIndex(COL_TIME_USED)), - data = cursor.getBlob(cursor.getColumnIndex(COL_DATA)) + id = cursor.getLong(COL_ID), + timeUsed = cursor.getLong(COL_TIME_USED), + data = cursor.getBlobOrNull(COL_DATA)!! ).apply { if (now - timeUsed >= 5 * 3600000L) { db.update( diff --git a/app/src/main/java/jp/juggler/util/ColumnMeta.kt b/app/src/main/java/jp/juggler/util/ColumnMeta.kt index 4346e6e7..04ebe6fe 100644 --- a/app/src/main/java/jp/juggler/util/ColumnMeta.kt +++ b/app/src/main/java/jp/juggler/util/ColumnMeta.kt @@ -52,9 +52,16 @@ fun Cursor.getStringOrNull(keyIdx: Int) = fun Cursor.getStringOrNull(key: String) = getStringOrNull(getColumnIndex(key)) +fun Cursor.getBlobOrNull(keyIdx: Int) = + if (isNull(keyIdx)) null else getBlob(keyIdx) + +fun Cursor.getBlobOrNull(key: String) = + getBlobOrNull(getColumnIndex(key)) + ///////////////////////////////////////////////////////////// interface TableCompanion { + val table: String fun onDBCreate(db: SQLiteDatabase) fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) } diff --git a/app/src/test/java/jp/juggler/subwaytooter/TestArrayListSizeBug.kt b/app/src/test/java/jp/juggler/subwaytooter/TestArrayListSizeBug.kt new file mode 100644 index 00000000..352398ab --- /dev/null +++ b/app/src/test/java/jp/juggler/subwaytooter/TestArrayListSizeBug.kt @@ -0,0 +1,23 @@ +package jp.juggler.subwaytooter + +import org.junit.Assert.assertEquals +import org.junit.Test + +class TestArrayListSizeBug { + + @Test + fun testArrayListSize() { + val list = ArrayList(arrayOf("c", "b", "a").toList()) + assertEquals("ArrayList size", 3, list.size) + } + + class ArrayListDerived(args: List) : ArrayList(args) + + @Test + fun testArrayListDerived() { + val list = ArrayListDerived(arrayOf("c", "b", "a").toList()) + assertEquals("ArrayListDerived size", 3, list.size) + // kotlin 1.5.31で(Javaの) size() ではなく getSize() にアクセスしようとして例外を出していた + // kotlin 1.5.30では大丈夫だったが、JetPack Composeは 1.5.31を要求するのだった…。 + } +} diff --git a/build.gradle b/build.gradle index 9c3899a8..5ec0c4eb 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { ext.lifecycle_version="2.4.0-rc01" ext.arch_version = "2.1.0" - ext.kotlin_version = '1.5.31' + ext.kotlin_version = '1.5.30' ext.kotlinx_coroutines_version = '1.5.2' ext.anko_version='0.10.8' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 53636 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ⅅ8zCNKueM5c!$)^mKpZNR!eIlFST57ePGQcqCqedAQ3UaUEzpjM--5V4YO zY22VxQm%$2NDnwfK+jkz=i2>NjAM6&P1DdcO<*Xs1-lzdXWn#LGSxwhPH7N%D8-zCgpFWt@`LgNYI+Fh^~nSiQmwH0^>E>*O$47MqfQza@Ce z1wBw;igLc#V2@y-*~Hp?jA1)+MYYyAt|DV_8RQCrRY@sAviO}wv;3gFdO>TE(=9o? z=S(r=0oT`w24=ihA=~iFV5z$ZG74?rmYn#eanx(!Hkxcr$*^KRFJKYYB&l6$WVsJ^ z-Iz#HYmE)Da@&seqG1fXsTER#adA&OrD2-T(z}Cwby|mQf{0v*v3hq~pzF`U`jenT z=XHXeB|fa?Ws$+9ADO0rco{#~+`VM?IXg7N>M0w1fyW1iiKTA@p$y zSiAJ%-Mg{m>&S4r#Tw@?@7ck}#oFo-iZJCWc`hw_J$=rw?omE{^tc59ftd`xq?jzf zo0bFUI=$>O!45{!c4?0KsJmZ#$vuYpZLo_O^oHTmmLMm0J_a{Nn`q5tG1m=0ecv$T z5H7r0DZGl6be@aJ+;26EGw9JENj0oJ5K0=^f-yBW2I0jqVIU};NBp*gF7_KlQnhB6 z##d$H({^HXj@il`*4^kC42&3)(A|tuhs;LygA-EWFSqpe+%#?6HG6}mE215Z4mjO2 zY2^?5$<8&k`O~#~sSc5Fy`5hg5#e{kG>SAbTxCh{y32fHkNryU_c0_6h&$zbWc63T z7|r?X7_H!9XK!HfZ+r?FvBQ$x{HTGS=1VN<>Ss-7M3z|vQG|N}Frv{h-q623@Jz*@ ziXlZIpAuY^RPlu&=nO)pFhML5=ut~&zWDSsn%>mv)!P1|^M!d5AwmSPIckoY|0u9I zTDAzG*U&5SPf+@c_tE_I!~Npfi$?gX(kn=zZd|tUZ_ez(xP+)xS!8=k(<{9@<+EUx zYQgZhjn(0qA#?~Q+EA9oh_Jx5PMfE3#KIh#*cFIFQGi)-40NHbJO&%ZvL|LAqU=Rw zf?Vr4qkUcKtLr^g-6*N-tfk+v8@#Lpl~SgKyH!+m9?T8B>WDWK22;!i5&_N=%f{__ z-LHb`v-LvKqTJZCx~z|Yg;U_f)VZu~q7trb%C6fOKs#eJosw&b$nmwGwP;Bz`=zK4 z>U3;}T_ptP)w=vJaL8EhW;J#SHA;fr13f=r#{o)`dRMOs-T;lp&Toi@u^oB_^pw=P zp#8Geo2?@!h2EYHY?L;ayT}-Df0?TeUCe8Cto{W0_a>!7Gxmi5G-nIIS;X{flm2De z{SjFG%knZoVa;mtHR_`*6)KEf=dvOT3OgT7C7&-4P#4X^B%VI&_57cBbli()(%zZC?Y0b;?5!f22UleQ=9h4_LkcA!Xsqx@q{ko&tvP_V@7epFs}AIpM{g??PA>U(sk$Gum>2Eu zD{Oy{$OF%~?B6>ixQeK9I}!$O0!T3#Ir8MW)j2V*qyJ z8Bg17L`rg^B_#rkny-=<3fr}Y42+x0@q6POk$H^*p3~Dc@5uYTQ$pfaRnIT}Wxb;- zl!@kkZkS=l)&=y|21veY8yz$t-&7ecA)TR|=51BKh(@n|d$EN>18)9kSQ|GqP?aeM ztXd9C&Md$PPF*FVs*GhoHM2L@D$(Qf%%x zwQBUt!jM~GgwluBcwkgwQ!249uPkNz3u@LSYZgmpHgX|P#8!iKk^vSKZ;?)KE$92d z2U>y}VWJ0&zjrIqddM3dz-nU%>bL&KU%SA|LiiUU7Ka|c=jF|vQ1V)Jz`JZe*j<5U6~RVuBEVJoY~ z&GE+F$f>4lN=X4-|9v*5O*Os>>r87u z!_1NSV?_X&HeFR1fOFb8_P)4lybJ6?1BWK`Tv2;4t|x1<#@17UO|hLGnrB%nu)fDk zfstJ4{X4^Y<8Lj<}g2^kksSefQTMuTo?tJLCh zC~>CR#a0hADw!_Vg*5fJwV{~S(j8)~sn>Oyt(ud2$1YfGck77}xN@3U_#T`q)f9!2 zf>Ia;Gwp2_C>WokU%(z2ec8z94pZyhaK+e>3a9sj^-&*V494;p9-xk+u1Jn#N_&xs z59OI2w=PuTErv|aNcK*>3l^W*p3}fjXJjJAXtBA#%B(-0--s;1U#f8gFYW!JL+iVG zV0SSx5w8eVgE?3Sg@eQv)=x<+-JgpVixZQNaZr}3b8sVyVs$@ndkF5FYKka@b+YAh z#nq_gzlIDKEs_i}H4f)(VQ!FSB}j>5znkVD&W0bOA{UZ7h!(FXrBbtdGA|PE1db>s z$!X)WY)u#7P8>^7Pjjj-kXNBuJX3(pJVetTZRNOnR5|RT5D>xmwxhAn)9KF3J05J; z-Mfb~dc?LUGqozC2p!1VjRqUwwDBnJhOua3vCCB-%ykW_ohSe?$R#dz%@Gym-8-RA zjMa_SJSzIl8{9dV+&63e9$4;{=1}w2=l+_j_Dtt@<(SYMbV-18&%F@Zl7F_5! z@xwJ0wiDdO%{}j9PW1(t+8P7Ud79yjY>x>aZYWJL_NI?bI6Y02`;@?qPz_PRqz(7v``20`- z033Dy|4;y6di|>cz|P-z|6c&3f&g^OAt8aN0Zd&0yZ>dq2aFCsE<~Ucf$v{sL=*++ zBxFSa2lfA+Y%U@B&3D=&CBO&u`#*nNc|PCY7XO<}MnG0VR764XrHtrb5zwC*2F!Lp zE<~Vj0;z!S-|3M4DFxuQ=`ShTf28<9p!81(0hFbGNqF%0gg*orez9!qt8e%o@Yfl@ zhvY}{@3&f??}7<`p>FyU;7?VkKbh8_=csozU=|fH&szgZ{=NDCylQ>EH^x5!K3~-V z)_2Y>0uJ`Z0Pb58y`RL+&n@m9tJ)O<%q#&u#DAIt+-rRt0eSe1MTtMl@W)H$b3D)@ z*A-1bUgZI)>HdcI4&W>P4W5{-j=s5p5`cbQ+{(g0+RDnz!TR^mxSLu_y#SDVKrj8i zA^hi6>jMGM;`$9Vfb-Yf!47b)Ow`2OKtNB=z|Kxa$5O}WPo;(Dc^`q(7X8kkeFyO8 z{XOq^07=u|7*P2`m;>PIFf=i80MKUxsN{d2cX0M+REsE*20+WQ79T9&cqT>=I_U% z{=8~^Isg(Nzo~`4iQfIb_#CVCD>#5h>=-Z#5dH}WxYzn%0)GAm6L2WdUdP=0_h>7f z(jh&7%1i(ZOn+}D8$iGK4Vs{pmHl_w4Qm-46H9>4^{3dz^DZDh+dw)6Xd@CpQNK$j z{CU;-cmpK=egplZ3y3%y=sEnCJ^eYVKXzV8H2_r*fJ*%*B;a1_lOpt6)IT1IAK2eB z{rie|uDJUrbgfUE>~C>@RO|m5ex55F{=~Bb4Cucp{ok7Yf9V}QuZ`#Gc|WaqsQlK- zKaV)iMRR__&Ak2Z=IM9R9g5$WM4u{a^C-7uX*!myEym z#_#p^T!P~#Dx$%^K>Y_nj_3J*E_LwJ60-5Xu=LkJAwcP@|0;a&+|+ZX`Jbj9P5;T% z|KOc}4*#4o{U?09`9Hz`Xo-I!P=9XfIrr*MQ}y=$!qgv?_J38^bNb4kM&_OVg^_=Eu-qG5U(fw0KMgH){C8pazq~51rN97hf#20-7=aK0)N|UM H-+%o-(+5aQ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c295eb39..ffed3a25 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ -distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip -distributionPath=wrapper/dists -zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 9d82f789..1b6c7873 100644 --- a/gradlew +++ b/gradlew @@ -1,74 +1,129 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -77,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -85,76 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,20 +24,23 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,34 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell