diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/BlameLoggerTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/BlameLoggerTest.kt new file mode 100644 index 000000000..443a0c8a6 --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/BlameLoggerTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +package com.android.aaptcompiler + +import com.android.utils.ILogger +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class BlameLoggerTest { + + @Test + fun rewriteSingleSource() { + val mockLogger = MockLogger() + val blameLogger = getMockBlameLogger(mockLogger) + + val result = blameLogger.getOriginalSource(BlameLogger.Source("foo/bar.xml", 3, 5)) + assertThat(result.sourcePath).isEqualTo("foo/bar.xml.rewritten") + assertThat(result.line).isEqualTo(4) + assertThat(result.column).isEqualTo(7) + } + + @Test + fun testsLoggedMessageRewritten() { + val mockLogger = MockLogger() + val blameLogger = getMockBlameLogger(mockLogger) + + blameLogger.error("Failed to read file", BlameLogger.Source("foo/bar.xml", 3, 5)) + assertThat(mockLogger.errors).hasSize(1) + val loggedError = mockLogger.errors.single() + assertThat(loggedError.first).contains("bar.xml.rewritten:4:7: Failed to read file") + assertThat(loggedError.second).isNull() + } + + class MockLogger: ILogger { + val warnings: MutableList = mutableListOf() + val infos: MutableList = mutableListOf() + val errors: MutableList> = mutableListOf() + val verboses: MutableList = mutableListOf() + + override fun warning(msgFormat: String, vararg args: Any?) { + warnings.add(String.format(msgFormat, args)) + } + + override fun info(msgFormat: String, vararg args: Any?) { + infos.add(String.format(msgFormat, args)) + } + + override fun error(t: Throwable?, msgFormat: String?, vararg args: Any?) { + errors.add(Pair(String.format(msgFormat!!, args), t)) + } + + override fun verbose(msgFormat: String, vararg args: Any?) { + verboses.add(String.format(msgFormat, args)) + } + } +} + +fun getMockBlameLogger(mockLogger: BlameLoggerTest.MockLogger) = + BlameLogger( + mockLogger + ) { + BlameLogger.Source(it.sourcePath + ".rewritten", it.line + 1, it.column + 2) + } diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ConfigDescriptionTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ConfigDescriptionTest.kt new file mode 100644 index 000000000..cd3b2e0d8 --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ConfigDescriptionTest.kt @@ -0,0 +1,132 @@ +package com.android.aaptcompiler + +import com.android.aaptcompiler.android.ResTableConfig +import com.google.common.truth.Truth +import org.junit.Test + +class ConfigDescriptionTest { + + private fun testParse(input: String): ConfigDescription? { + return try { + parse(input) + } catch (e: Exception) { + null + } + } + + @Test + fun failWhenQualifiersAreOutOfOrder() { + Truth.assertThat(testParse("en-sw600dp-ldrtl")).isNull() + Truth.assertThat(testParse("land-en")).isNull() + Truth.assertThat(testParse("hdpi-320dpi")).isNull() + } + + @Test + fun failWhenQualifiersAreNotMatched() { + Truth.assertThat(testParse("en-sw600dp-ILLEGAL")).isNull() + } + + @Test + fun failWhenQualifiersHaveTrailingDelimiter() { + Truth.assertThat(testParse("en-sw600dp-land-")).isNull() + } + + @Test + fun testParseBasic() { + val default = testParse("") + Truth.assertThat(default).isNotNull() + Truth.assertThat(default.toString()).isEqualTo("DEFAULT") + + val frenchLandscape = testParse("fr-land") + Truth.assertThat(frenchLandscape).isNotNull() + Truth.assertThat(frenchLandscape.toString()).isEqualTo("fr-land") + + val longConfig = + testParse("mcc310-pl-sw720dp-normal-long-port-night-xhdpi-keyssoft-qwerty-navexposed-nonav") + Truth.assertThat(longConfig).isNotNull() + Truth.assertThat(longConfig.toString()) + .isEqualTo( + "mcc310-pl-sw720dp-normal-long-port-night-xhdpi-keyssoft-qwerty-navexposed-nonav-v13") + } + + @Test + fun testLocales() { + val localeConfig = testParse("en-rUS") + Truth.assertThat(localeConfig).isNotNull() + Truth.assertThat(localeConfig.toString()).isEqualTo("en-rUS") + } + + @Test + fun testQualifierAddedInApi13() { + val widthConfig = testParse("sw600dp") + Truth.assertThat(widthConfig).isNotNull() + Truth.assertThat(widthConfig.toString()).isEqualTo("sw600dp-v13") + + val widthConfigWithLowerVersion = testParse("sw600dp-v8") + Truth.assertThat(widthConfigWithLowerVersion).isNotNull() + Truth.assertThat(widthConfigWithLowerVersion.toString()).isEqualTo("sw600dp-v13") + } + + @Test + fun testParseCarAttribute() { + val config = testParse("car") + Truth.assertThat(config).isNotNull() + Truth.assertThat(config!!.uiMode).isEqualTo(ResTableConfig.UI_MODE.TYPE_CAR) + } + + @Test + fun testParseVersionOnly() { + val config = testParse("v26") + Truth.assertThat(config).isNotNull() + Truth.assertThat(config!!.sdkVersion).isEqualTo(26) + } + + @Test + fun testParsingRoundQualifier() { + val roundConfig = testParse("round") + Truth.assertThat(roundConfig).isNotNull() + Truth.assertThat(roundConfig!!.layoutRound()) + .isEqualTo(ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_YES) + Truth.assertThat(roundConfig.sdkVersion).isEqualTo(23) + Truth.assertThat(roundConfig.toString()).isEqualTo("round-v23") + + val notRoundConfig = testParse("notround") + Truth.assertThat(notRoundConfig).isNotNull() + Truth.assertThat(notRoundConfig!!.layoutRound()) + .isEqualTo(ResTableConfig.SCREEN_LAYOUT2.SCREENROUND_NO) + Truth.assertThat(notRoundConfig.sdkVersion).isEqualTo(23) + Truth.assertThat(notRoundConfig.toString()).isEqualTo("notround-v23") + } + + @Test + fun testWideColorGamutQualifier() { + val wideConfig = testParse("widecg") + Truth.assertThat(wideConfig).isNotNull() + Truth.assertThat(wideConfig!!.wideColorGamut()) + .isEqualTo(ResTableConfig.COLOR_MODE.WIDE_GAMUT_YES) + Truth.assertThat(wideConfig.sdkVersion).isEqualTo(26) + Truth.assertThat(wideConfig.toString()).isEqualTo("widecg-v26") + + val noWideConfig = testParse("nowidecg") + Truth.assertThat(noWideConfig).isNotNull() + Truth.assertThat(noWideConfig!!.wideColorGamut()) + .isEqualTo(ResTableConfig.COLOR_MODE.WIDE_GAMUT_NO) + Truth.assertThat(noWideConfig.sdkVersion).isEqualTo(26) + Truth.assertThat(noWideConfig.toString()).isEqualTo("nowidecg-v26") + } + + @Test + fun testHdrQualifier() { + val highDRConfig = testParse("highdr") + Truth.assertThat(highDRConfig).isNotNull() + Truth.assertThat(highDRConfig!!.hdr()).isEqualTo(ResTableConfig.COLOR_MODE.HDR_YES) + Truth.assertThat(highDRConfig.sdkVersion).isEqualTo(26) + Truth.assertThat(highDRConfig.toString()).isEqualTo("highdr-v26") + + val lowDRConfig = testParse("lowdr") + Truth.assertThat(lowDRConfig).isNotNull() + Truth.assertThat(lowDRConfig!!.hdr()).isEqualTo(ResTableConfig.COLOR_MODE.HDR_NO) + Truth.assertThat(lowDRConfig.sdkVersion).isEqualTo(26) + Truth.assertThat(lowDRConfig.toString()).isEqualTo("lowdr-v26") + } +} diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/FloatParsingTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/FloatParsingTest.kt new file mode 100644 index 000000000..66e88a2f0 --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/FloatParsingTest.kt @@ -0,0 +1,335 @@ +package com.android.aaptcompiler + +import com.google.common.truth.Truth +import org.junit.Ignore +import org.junit.Test + +class FloatParsingTest { + + @Test + fun testParseDecFloatZero() { + testParseDecSupported("0", 0.0.toFloat()) + testParseDecSupported("-0", -(0.0.toFloat())) + + testParseDecSupported("+0.0", 0.0.toFloat()) + testParseDecSupported("0.", 0.0.toFloat()) + testParseDecSupported(".0", 0.0.toFloat()) + testParseDecSupported("0000000000000.0000000000000", 0.0.toFloat()) + testParseDecSupported("-0.0", -(0.0.toFloat())) + testParseDecSupported("0e0", 0.0.toFloat()) + testParseDecSupported("0e24", 0.0.toFloat()) + testParseDecSupported("0e-14", 0.0.toFloat()) + + // Zero's with absurdly large/small exponents should still parse properly. + testParseDecSupported("0e9001", 0.0.toFloat()) + testParseDecSupported("0e+9001", 0.0.toFloat()) + testParseDecSupported("0e-9001", 0.0.toFloat()) + } + + @Test + fun testParseHexFloatZero() { + testParseHexNormal("0x0", 0.0.toFloat()) + + testParseHexNormal("+0x0", 0.0.toFloat()) + + testParseHexNormal("0x.0", 0.0.toFloat()) + + testParseHexNormal("0x0.", 0.0.toFloat()) + + testParseHexNormal("0x0000000000.0000000000", 0.0.toFloat()) + + testParseHexNormal("-0x0", -(0.0.toFloat())) + + testParseHexNormal("0x0p0", 0.0.toFloat()) + + testParseHexNormal("0x0p12", 0.0.toFloat()) + + testParseHexNormal("0x0p-14", 0.0.toFloat()) + + // Zero's with absurdly large/small exponents should still work. + testParseHexNormal("0x0p256", 0.0.toFloat()) + + testParseHexNormal("0x0p-256", 0.0.toFloat()) + } + + @Test + fun testParseDecIntegerValue() { + // Basic Integer Parsing + testParseDecSupported("10", 10.toFloat()) + testParseDecSupported("12345", 12345.toFloat()) + testParseDecSupported("67890", 67890.toFloat()) + + // Super-Integer parsing + // 2^32 is larger than int max + testParseDecSupported("4294967296", 4294967296.toFloat()) + testParseDecSupported("-4294967296", (-4294967296).toFloat()) + + testParseDecSupported("3.4028235e38", 3.4028235e38.toFloat()) + testParseDecSupported("-3.4028235e38", (-3.4028235e38).toFloat()) + + // Anything larger should certainly fail + testParseDecNotSupported("3.4028236e38", Float.POSITIVE_INFINITY) + testParseDecNotSupported("-3.4028236e38", Float.NEGATIVE_INFINITY) + } + + @Test + fun testParseHexIntegerValue() { + // Basic Integer Parsing + testParseHexNormal("0x10", 16.toFloat()) + + testParseHexNormal("0x1p4", 16.toFloat()) + + testParseHexNormal("0x0123", 0x123.toFloat()) + + testParseHexNormal("0x4567", 0x4567.toFloat()) + + testParseHexNormal("0x89ab", 0x89ab.toFloat()) + + testParseHexNormal("0xcdef", 0xcdef.toFloat()) + + // Super-Integer parsing + // 2^32 is larger than integer max + testParseHexNormal("0x100000000", 4294967296.toFloat()) + + testParseHexNormal("-0x100000000", (-4294967296).toFloat()) + + // Max normal float value + testParseHexNormal("0x1.fffffeP127", 3.4028235e38.toFloat()) + testParseHexNormal("-0x1.fffffeP127", (-3.4028235e38).toFloat()) + + // Anything larger should fail + testParseHexNonnormal("0x1.ffffffP127", Float.POSITIVE_INFINITY) + testParseHexNonnormal("-0x1.ffffffP127", Float.NEGATIVE_INFINITY) + + // Rounding. + testParseHexNormal("0x2000001", 0x2000002.toFloat()) + testParseHexNormal("-0x2000001", (-0x2000002).toFloat()) + // This rounding test will assure that the exponent is adjusted properly. + testParseHexNormal("0x1ffffff", 0x2000000.toFloat()) + testParseHexNormal("-0x1ffffff", (-0x2000000).toFloat()) + } + + @Test + fun testParseDecSubIntegerValue() { + // Basic Sub-Integer parsing + testParseDecSupported(".1", .1.toFloat()) + + //testParseDecSupported("2.25", 2.25.toFloat()) + testParseDecSupported("-2.25", (-2.25).toFloat()) + + // Minimum normal float value + testParseDecSupported("1.17549435e-38", 1.17549435e-38.toFloat()) + testParseDecSupported("-1.17549435e-38", (-1.17549435e-38).toFloat()) + + // Largest denormal number should fail on decimal parse, + // i.e. negative = 0x0/0x1 exponent = 0x0 mantissa = 0x7fffff + testParseDecNotSupported("1.1754942e-38", 1.1754942e-38.toFloat()) + testParseDecNotSupported("-1.1754942e-38", (-1.1754942e-38).toFloat()) + + // Tiny numbers are interpreted as denormal by Kotlin and are rounded to 0.0. + // We do not support those values. + testParseDecNotSupported("1e-100", 0.00.toFloat()) + testParseDecNotSupported("-1e-100", -(0.00.toFloat())) + } + + @Test + fun testParseHexSubIntegerValue() { + // Basic Sub-Integer parsing + testParseHexNormal("0x.8", .5.toFloat()) + + testParseHexNormal("0x2.4", 2.25.toFloat()) + + testParseHexNormal("-0x2.4", (-2.25).toFloat()) + + // Minimum normal float value + testParseHexNormal("0x1p-126", 1.17549435e-38.toFloat()) + testParseHexNormal("-0x1p-126", (-1.17549435e-38).toFloat()) + + // Edge case for denormal number rounding with less precision to minimum normal float. + testParseHexNormal("0x1.fffffeP-127", 1.17549435e-38.toFloat()) + testParseHexNormal("-0x1.fffffeP-127", (-1.17549435e-38).toFloat()) + + // Largest denormal number should fail on hex parse, + // i.e. negative = 0x0/0x1 exponent = 0x0 mantissa = 0x7fffff + testParseHexNonnormal("0x1.fffffdp-127", 1.1754942e-38.toFloat()) + testParseHexNonnormal("-0x1.fffffdp-127", (-1.1754942e-38).toFloat()) + + // Tiny numbers are interpreted as denormal by Kotlin and are rounded to 0.0. + // We do not support those values. + testParseHexNonnormal("0x1p-255", 0.0.toFloat()) + testParseHexNonnormal("-0x1p-255", (-0.0).toFloat()) + } + + @Test + fun testParseDecFailures() { + testDecFailure(".") + testDecFailure("-.") + testDecFailure("+.") + + testDecFailure("+e3") + testDecFailure("-e-2") + + testDecFailure("125e") + testDecFailure("-125e") + testDecFailure("+125e") + + testDecFailure("e") + testDecFailure("-e") + + testDecFailure("1.2.3") + testDecFailure("-1.2.3") + + testDecFailure("+-12") + testDecFailure("--0") + testDecFailure("++42") + } + + @Test + fun testParseHexFailures() { + testHexFailure("0x.") + testHexFailure("-0x.") + + testHexFailure("0xp") + testHexFailure("-0xp") + + testHexFailure("0xp0") + testHexFailure("-0xp0") + + testHexFailure("0x12p") + testHexFailure("-0x12p") + + testHexFailure("0x1.2.1") + testHexFailure("-0x1.2.1") + + testHexFailure("0x1.2.1p0") + testHexFailure("-0x1.2.1p0") + + testHexFailure("--0x0") + + testHexFailure("+-0x0p0") + + testHexFailure("-+0x0p0") + + testHexFailure("++0x0p0") + } + + /** + * Round trip checks all supported float values. + * + * A round trip of a floating point value, goes as follows: + * + * Take a floating point number, in it's binary format. + * + * Call [Float.toString] getting the string representation. + * + * Call [parseFloat] on the string representation. + * + * Ensure that the initial float and it's parsed result, are the same. + * + * This tests goes over ~2 billion different float values, and takes ~1 hour for full + * computation. As such this test is set to be ignored, but should be enabled if float parsing + * is modified significantly. + */ + @Test + @Ignore("b/303076368") + fun roundTrip() { + var currentVal = 0x00800000 + val maxVal = 0x7f000000 + var err = "" + var numErrors = 0 + while (currentVal < maxVal) { + val float = Float.fromBits(currentVal) + val string = float.toString() + val parse = parseFloat(string) + if (float != parse) { + if (numErrors < 1000) { + err += "expected: $float actual: $parse \n" + ++numErrors + } else if (numErrors == 1000) { + err += "and more...\n" + break + } + } + ++currentVal + } + if (err.isNotEmpty()) { + error(err) + } + } + + private fun needsModificationForKotlin(hexText: String): Boolean { + return !hexText.lowercase().contains('p') && hexText.all { + when (it) { + in '0'..'9' -> true + in 'a'..'f' -> true + in 'A'..'F' -> true + 'x', 'X' -> true + '.', '-', '+' -> true + else -> false + } + } + } + + private fun testParseDecSupported(text: String, expected: Float) { + val parsedFloat = parseFloat(text) + Truth.assertThat(parsedFloat!!.toRawBits().toString(16)) + .isEqualTo(expected.toRawBits().toString(16)) + Truth.assertThat(parsedFloat).isEqualTo(text.toFloat()) + } + + private fun testParseDecNotSupported(text: String, expected: Float) { + val parsedFloat = parseFloat(text) + Truth.assertThat(parsedFloat).isNull() + Truth.assertThat(text.toFloat()).isEqualTo(expected) + } + + private fun testDecFailure(text: String) { + Truth.assertThat(parseFloat(text)).isNull() + Truth.assertThat(text.toFloatOrNull()).isNull() + } + + /** + * Tests a parseHex value that is expected to succeed. This must have the same result as the + * String.toFloat() method, with the possible addition of a "p0" to the text to make it compatible + * with Kotlin's toFloat method. + * + * Specifically, the hex parsing functionality works for all "normal" floating point numbers. + * (i.e.) floating point values that do not have a special value in the exponent to force + **/ + private fun testParseHexNormal(text: String, expected: Float) { + val parsedFloat = parseFloat(text) + Truth.assertThat(parsedFloat).isEqualTo(expected) + // We need to append p0 for default float parsing that doesn't already have an exponent marker. + // As this is how Kotlin does it normally. + val modifiedText = if (needsModificationForKotlin(text)) text + "p0" else text + Truth.assertThat(parsedFloat).isEqualTo(modifiedText.toFloat()) + } + + /** + * Tests a parseHex value with a subnormal (denormal) or special values. parseFloat() should fail + * in these cases, but the text (with the possible addition of "p0") should succeed when parsing + * normally in Kotlin's String.toFloat() method. + */ + private fun testParseHexNonnormal(text: String, expected: Float) { + val parsedFloat = parseFloat(text) + Truth.assertThat(parsedFloat).isNull() + + val modifiedText = if (needsModificationForKotlin(text)) text + "p0" else text + if (expected.isNaN()) { + Truth.assertThat(modifiedText.toFloat()).isNaN() + } else { + Truth.assertThat(modifiedText.toFloat()).isEqualTo(expected) + } + } + + /** + * Tests a failure to parse and ensures Kotlin has the same behavior. + */ + private fun testHexFailure(text: String) { + Truth.assertThat(parseFloat(text)).isNull() + val modifiedText = if (needsModificationForKotlin(text)) text + "p0" else text + Truth.assertThat(modifiedText.toFloatOrNull()).isNull() + } + + +} diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/LocaleValueTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/LocaleValueTest.kt new file mode 100644 index 000000000..6a1a21c7c --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/LocaleValueTest.kt @@ -0,0 +1,57 @@ +package com.android.aaptcompiler + +import com.android.aaptcompiler.android.ResTableConfig +import com.google.common.truth.Truth +import org.junit.Test +import java.nio.charset.StandardCharsets + +class LocaleValueTest { + + fun testLanguage(input: String, expectedLang: String, expectFailure: Boolean = false) { + val parts = input.split('-').map { it.lowercase() } + val locale = LocaleValue() + + val count = locale.initFromParts(parts, 0) + + Truth.assertThat(count).isEqualTo(if (expectFailure) 0 else 1) + Truth.assertThat(locale.language).isEqualTo(expectedLang) + } + + fun testLanguageRegion(input: String, expectedLang: String, expectedRegion: String) { + val parts = input.split('-').map { it.lowercase() } + val locale = LocaleValue() + + val count = locale.initFromParts(parts, 0) + + Truth.assertThat(count).isEqualTo(2) + Truth.assertThat(locale.language).isEqualTo(expectedLang) + Truth.assertThat(locale.region).isEqualTo(expectedRegion) + } + + @Test + fun testParseLanguage() { + testLanguage("en", "en") + testLanguage("fr", "fr") + testLanguage("land", "", true) + testLanguage("fr-land", "fr") + + testLanguageRegion("fr-rCA", "fr","CA") + } + + @Test + fun testWritingLocaleToResTableConfig() { + val languageLocaleValue = "b+be+tarask" + val parts = languageLocaleValue.split('-').map(String::lowercase) + val locale = LocaleValue() + locale.initFromParts(parts, 0) + + val resTableConfig = ResTableConfig() + locale.writeTo(resTableConfig) + Truth.assertThat(resTableConfig.language.toString(StandardCharsets.UTF_8)) + .isEqualTo("be") + Truth.assertThat(resTableConfig.localeVariant) + // bytearray is utf-8 representation of 'tarask' followed by two null values + // as it is 6 bytes in length but represented by 8 bytes. + .isEqualTo(byteArrayOf(116, 97, 114, 97, 115, 107, 0, 0)) + } +} diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/PseudolocaleGeneratorTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/PseudolocaleGeneratorTest.kt new file mode 100644 index 000000000..9092527a4 --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/PseudolocaleGeneratorTest.kt @@ -0,0 +1,335 @@ +package com.android.aaptcompiler + +import com.google.common.truth.Truth +import org.junit.Test + +class PseudolocaleGeneratorTest { + + @Test + fun testPseudolocalizeBasicString() { + val pool = StringPool() + val original = + BasicString(pool.makeRef("Hello world!"), listOf(UntranslatableSection(1, 4))) + + val newString1 = pseudolocalizeBasicString(original, Pseudolocalizer.Method.NONE, pool) + + Truth.assertThat(newString1.ref.value()).isEqualTo(original.ref.value()) + + val newString2 = pseudolocalizeBasicString(original, Pseudolocalizer.Method.ACCENT, pool) + + Truth.assertThat(newString2.ref.value()).isEqualTo("[Ĥellö ŵöŕļð¡ one two]") + + val newString3 = pseudolocalizeBasicString(original, Pseudolocalizer.Method.BIDI, pool) + + Truth.assertThat(newString3.ref.value()) + .isEqualTo("${bidiWordStart}H${bidiWordEnd}ell${bidiWordStart}o$bidiWordEnd " + + "${bidiWordStart}world!$bidiWordEnd") + } + + @Test + fun testPseudolocalizeStyledString() { + val pool = StringPool() + val spans = listOf( + Span("i", 1, 10), + Span("b", 2, 3), + Span("b", 6, 7)) + val original = StyledString(pool.makeRef(StyleString("Hello world!", spans)), listOf()) + + val newString1 = pseudolocalizeStyledString(original, Pseudolocalizer.Method.NONE, pool) + + Truth.assertThat(newString1.ref.value()).isEqualTo(original.ref.value()) + Truth.assertThat(newString1.spans().size).isEqualTo(original.spans().size) + + val spans1 = newString1.spans() + Truth.assertThat(spans1[0].name.value()).isEqualTo("i") + Truth.assertThat(spans1[0].firstChar).isEqualTo("H".length) + Truth.assertThat(spans1[0].lastChar).isEqualTo("Hello worl".length) + + Truth.assertThat(spans1[1].name.value()).isEqualTo("b") + Truth.assertThat(spans1[1].firstChar).isEqualTo("He".length) + Truth.assertThat(spans1[1].lastChar).isEqualTo("Hel".length) + + Truth.assertThat(spans1[2].name.value()).isEqualTo("b") + Truth.assertThat(spans1[2].firstChar).isEqualTo("Hello ".length) + Truth.assertThat(spans[2].lastChar).isEqualTo("Hello w".length) + + val newString2 = pseudolocalizeStyledString(original, Pseudolocalizer.Method.ACCENT, pool) + + Truth.assertThat(newString2.ref.value()).isEqualTo("[Ĥéļļö ŵöŕļð¡ one two]") + Truth.assertThat(newString2.spans().size).isEqualTo(original.spans().size) + + val spans2 = newString2.spans() + Truth.assertThat(spans2[0].name.value()).isEqualTo("i") + Truth.assertThat(spans2[0].firstChar).isEqualTo("[Ĥ".length) + Truth.assertThat(spans2[0].lastChar).isEqualTo("[Ĥéļļö ŵöŕļ".length) + + Truth.assertThat(spans2[1].name.value()).isEqualTo("b") + Truth.assertThat(spans2[1].firstChar).isEqualTo("[Ĥé".length) + Truth.assertThat(spans2[1].lastChar).isEqualTo("[Ĥéļ".length) + + Truth.assertThat(spans2[2].name.value()).isEqualTo("b") + Truth.assertThat(spans2[2].firstChar).isEqualTo("[Ĥéļļö ".length) + Truth.assertThat(spans2[2].lastChar).isEqualTo("[Ĥéļļö ŵ".length) + } + + @Test + fun testPseudolocalizeAdjacentNestedTags() { + val pool = StringPool() + val originalSpans = listOf(Span("b", 0, 3), Span("i", 0, 3)) + + val original = StyledString(pool.makeRef(StyleString("bold", originalSpans)), listOf()) + + val newString = pseudolocalizeStyledString(original, Pseudolocalizer.Method.ACCENT, pool) + + Truth.assertThat(newString.ref.value()).isEqualTo("[ɓöļð one]") + Truth.assertThat(newString.spans().size).isEqualTo(original.spans().size) + + val spans = newString.spans() + + Truth.assertThat(spans[0].name.value()).isEqualTo("b") + Truth.assertThat(spans[0].firstChar).isEqualTo("[".length) + Truth.assertThat(spans[0].lastChar).isEqualTo("[ɓöļ".length) + + Truth.assertThat(spans[1].name.value()).isEqualTo("i") + Truth.assertThat(spans[1].firstChar).isEqualTo("[".length) + Truth.assertThat(spans[1].lastChar).isEqualTo("[ɓöļ".length) + } + + @Test + fun testPseudolocalizeUnsorted() { + val pool = StringPool() + val originalSpans = listOf(Span("i", 2, 3), Span("b", 0, 1)) + val original = StyledString(pool.makeRef(StyleString("bold", originalSpans)), listOf()) + + val newString = pseudolocalizeStyledString(original, Pseudolocalizer.Method.ACCENT, pool) + + Truth.assertThat(newString.ref.value()).isEqualTo("[ɓöļð one]") + Truth.assertThat(newString.spans().size).isEqualTo(original.spans().size) + + val spans = newString.spans() + + Truth.assertThat(spans[0].name.value()).isEqualTo("b") + Truth.assertThat(spans[0].firstChar).isEqualTo("[".length) + Truth.assertThat(spans[0].lastChar).isEqualTo("[ɓ".length) + + Truth.assertThat(spans[1].name.value()).isEqualTo("i") + Truth.assertThat(spans[1].firstChar).isEqualTo("[ɓö".length) + Truth.assertThat(spans[1].lastChar).isEqualTo("[ɓöļ".length) + } + + @Test + fun testPseudolocalizeNestedAndAdjacentTags() { + val pool = StringPool() + val originalSpans = listOf( + Span("b", 16, 19), + Span("em", 29, 47), + Span("i", 38, 40), + Span("b", 44, 47)) + val originalText = "This sentence is not what you think it is at all." + val original = StyledString(pool.makeRef(StyleString(originalText, originalSpans)), listOf()) + + val newString = pseudolocalizeStyledString(original, Pseudolocalizer.Method.ACCENT, pool) + + Truth.assertThat(newString.ref.value()) + .isEqualTo("[Ţĥîš šéñţéñçé îš ñöţ ŵĥåţ ýöû ţĥîñķ îţ îš åţ åļļ. one two three four five six]") + Truth.assertThat(newString.spans().size).isEqualTo(original.spans().size) + + val spans = newString.spans() + Truth.assertThat(spans[0].name.value()).isEqualTo("b") + Truth.assertThat(spans[0].firstChar).isEqualTo("[Ţĥîš šéñţéñçé îš".length) + Truth.assertThat(spans[0].lastChar).isEqualTo("[Ţĥîš šéñţéñçé îš ñö".length) + + Truth.assertThat(spans[1].name.value()).isEqualTo("em") + Truth.assertThat(spans[1].firstChar).isEqualTo("[Ţĥîš šéñţéñçé îš ñöţ ŵĥåţ ýöû".length) + Truth.assertThat(spans[1].lastChar) + .isEqualTo("[Ţĥîš šéñţéñçé îš ñöţ ŵĥåţ ýöû ţĥîñķ îţ îš åţ åļ".length) + + Truth.assertThat(spans[2].name.value()).isEqualTo("i") + Truth.assertThat(spans[2].firstChar).isEqualTo("[Ţĥîš šéñţéñçé îš ñöţ ŵĥåţ ýöû ţĥîñķ îţ".length) + Truth.assertThat(spans[2].lastChar) + .isEqualTo("[Ţĥîš šéñţéñçé îš ñöţ ŵĥåţ ýöû ţĥîñķ îţ î".length) + + Truth.assertThat(spans[3].name.value()).isEqualTo("b") + Truth.assertThat(spans[3].firstChar) + .isEqualTo("[Ţĥîš šéñţéñçé îš ñöţ ŵĥåţ ýöû ţĥîñķ îţ îš åţ".length) + Truth.assertThat(spans[3].lastChar) + .isEqualTo("[Ţĥîš šéñţéñçé îš ñöţ ŵĥåţ ýöû ţĥîñķ îţ îš åţ åļ".length) + } + + @Test + fun testPseudolocalizePartsOfStyledString() { + val pool = StringPool() + val originalSpans = listOf(Span("em", 4, 14), Span("i", 18, 33)) + val untranslatables = listOf(UntranslatableSection(11, 15)) + val originalText = "This should NOT be pseudolocalized." + val original = + StyledString(pool.makeRef(StyleString(originalText, originalSpans)), untranslatables) + + val newString = pseudolocalizeStyledString(original, Pseudolocalizer.Method.ACCENT, pool) + + Truth.assertThat(newString.ref.value()) + .isEqualTo("[Ţĥîš šĥöûļð NOT ɓé þšéûðöļöçåļîžéð. one two three four]") + Truth.assertThat(newString.spans().size).isEqualTo(original.spans().size) + + val spans = newString.spans() + Truth.assertThat(spans[0].name.value()).isEqualTo("em") + Truth.assertThat(spans[0].firstChar).isEqualTo("[Ţĥîš".length) + Truth.assertThat(spans[0].lastChar).isEqualTo("[Ţĥîš šĥöûļð NO".length) + + Truth.assertThat(spans[1].name.value()).isEqualTo("i") + Truth.assertThat(spans[1].firstChar).isEqualTo("[Ţĥîš šĥöûļð NOT ɓé".length) + Truth.assertThat(spans[1].lastChar).isEqualTo("[Ţĥîš šĥöûļð NOT ɓé þšéûðöļöçåļîžé".length) + } + + @Test + fun testPseudolocalizeOnlyDefaultConfigs() { + val table = ResourceTable() + + val string1 = BasicString(table.stringPool.makeRef("one")) + table.addResource( + parseResourceName("android:string/one")!!.resourceName, + ConfigDescription(), + "", + string1) + + val string2 = BasicString(table.stringPool.makeRef("two")) + table.addResource( + parseResourceName("android:string/two")!!.resourceName, + parse("en"), + "", + string2) + + val string3Default = BasicString(table.stringPool.makeRef("three")) + table.addResource( + parseResourceName("android:string/three")!!.resourceName, + ConfigDescription(), + "", + string3Default) + + val string3Override = BasicString(table.stringPool.makeRef("three next")) + table.addResource( + parseResourceName("android:string/three")!!.resourceName, + parse("en-rXA"), + "", + string3Override) + + val string4 = BasicString(table.stringPool.makeRef("four")) + table.addResource( + parseResourceName("android:string/four")!!.resourceName, + ConfigDescription(), + "", + string4) + + val resource4 = getValue(table, "android:string/four") + // sanity check + Truth.assertThat(resource4).isNotNull() + resource4!!.translatable = false + + val generator = PseudolocaleGenerator() + generator.consume(table) + + // Normal pseudolocalization should take place. + Truth.assertThat(getValue(table, "android:string/one", parse("en-rXA"))).isNotNull() + Truth.assertThat(getValue(table, "android:string/one", parse("ar-rXB"))).isNotNull() + + // No default config for android:string/two, so no pseudlocales should exist. + Truth.assertThat(getValue(table, "android:string/two", parse("en-rXA"))).isNull() + Truth.assertThat(getValue(table, "android:string/two", parse("ar-rXB"))).isNull() + + // Check that we didn't override manual pseudolocalization. + val resource3Override = getValue(table, "android:string/three", parse("en-rXA")) + + Truth.assertThat(resource3Override).isNotNull() + Truth.assertThat((resource3Override as BasicString).ref.value()).isEqualTo("three next") + + // Check that four's translatable marker was honored. + Truth.assertThat(getValue(table, "android:string/four", parse("en-rXA"))).isNull() + Truth.assertThat(getValue(table, "android:string/four", parse("ar-rXB"))).isNull() + } + + @Test + fun testPluralsArePseudolocalized() { + val table = ResourceTable() + + val plural = Plural() + plural.setValue(Plural.Type.ZERO, BasicString(table.stringPool.makeRef("zero"))) + plural.setValue(Plural.Type.ONE, BasicString(table.stringPool.makeRef("one"))) + + table.addResource( + parseResourceName("com.pkg:plurals/foo")!!.resourceName, ConfigDescription(), "", plural) + + val expected = Plural() + expected.setValue(Plural.Type.ZERO, BasicString(table.stringPool.makeRef("[žéŕö one]"))) + expected.setValue(Plural.Type.ONE, BasicString(table.stringPool.makeRef("[öñé one]"))) + + val generator = PseudolocaleGenerator() + generator.consume(table) + + val actual = getValue(table, "com.pkg:plurals/foo", parse("en-rXA")) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun testUntranslatableSections() { + val table = ResourceTable() + + val originalText = "Hello world!" + val originalSpans = listOf(Span("i", 1, 10), Span("b", 2, 3), Span("b", 6, 7)) + val originalStyle = StyledString( + table.stringPool.makeRef(StyleString(originalText, originalSpans)), + listOf(UntranslatableSection(6, 8), UntranslatableSection(8, 11))) + + val originalString = BasicString( + table.stringPool.makeRef(originalText), + listOf(UntranslatableSection(6, 11))) + + table.addResource( + parseResourceName("android:string/foo")!!.resourceName, + ConfigDescription(), + "", + originalStyle) + table.addResource( + parseResourceName("android:string/bar")!!.resourceName, + ConfigDescription(), + "", + originalString) + + val generator = PseudolocaleGenerator() + generator.consume(table) + + val newStyle = getValue(table, "android:string/foo", parse("en-rXA")) + Truth.assertThat(newStyle).isNotNull() + + // "world" should be untranslated. + Truth.assertThat((newStyle as StyledString).ref.value()).contains("world") + + val newString = getValue(table, "android:string/bar", parse("en-rXA")) + Truth.assertThat(newString).isNotNull() + + // "world" should be untranslated. + Truth.assertThat((newString as BasicString).ref.value()).contains("world") + } + + // Regression test for b/188963894. + @Test + fun canPseudolocalizeNestedTagWithSpace() { + val pool = StringPool() + val originalSpans = listOf( + Span("b", 0, 4), + Span("i", 0, 10) + ) + val originalText = "hello world" + val original = StyledString(pool.makeRef(StyleString(originalText, originalSpans)), listOf()) + + val newString = pseudolocalizeStyledString(original, Pseudolocalizer.Method.ACCENT, pool) + val spans = newString.spans() + Truth.assertThat(spans[0].name.value()).isEqualTo("i") + Truth.assertThat(spans[0].firstChar).isEqualTo(1) + Truth.assertThat(spans[0].lastChar).isEqualTo("ĥéļļö ŵöŕļð".length) + + Truth.assertThat(spans[1].name.value()).isEqualTo("b") + Truth.assertThat(spans[1].firstChar).isEqualTo(1) + Truth.assertThat(spans[1].lastChar).isEqualTo("ĥéļļö".length) + } +} diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/PseudolocalizerTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/PseudolocalizerTest.kt new file mode 100644 index 000000000..7bbaa98cf --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/PseudolocalizerTest.kt @@ -0,0 +1,253 @@ +package com.android.aaptcompiler + +import com.google.common.truth.Truth +import org.junit.Test + +internal val bidiWordStart = String(byteArrayOf( + 0xe2.toByte(), + 0x80.toByte(), + 0x8f.toByte(), + 0xe2.toByte(), + 0x80.toByte(), + 0xae.toByte())) +internal val bidiWordEnd = String(byteArrayOf( + 0xe2.toByte(), + 0x80.toByte(), + 0xac.toByte(), + 0xe2.toByte(), + 0x80.toByte(), + 0x8f.toByte())) + +class PseudolocalizerTest() { + private fun simpleTest(input: String, expected: String, method: Pseudolocalizer.Method) { + val localizer = Pseudolocalizer(method) + val result = localizer.start() + localizer.text(input) + localizer.end() + Truth.assertThat(result).isEqualTo(expected) + } + + private fun compoundTest( + expected: String, method: Pseudolocalizer.Method, vararg inputs: String) { + + val localizer = Pseudolocalizer(method) + val result = StringBuilder().append(localizer.start()) + for (input in inputs) { + result.append(localizer.text(input)) + } + result.append(localizer.end()) + Truth.assertThat(result.toString()).isEqualTo(expected) + } + + @Test + fun testNoPseudolocalization() { + simpleTest("", "", Pseudolocalizer.Method.NONE) + simpleTest("Hello, world", "Hello, world", Pseudolocalizer.Method.NONE) + compoundTest("Hello, world", Pseudolocalizer.Method.NONE, "Hello,", " world") + } + + @Test + fun testPlainTextAccent() { + simpleTest("", "[]", Pseudolocalizer.Method.ACCENT) + simpleTest("Hello, world", "[Ĥéļļö, ŵöŕļð one two]", Pseudolocalizer.Method.ACCENT) + simpleTest("Hello, %1d", "[Ĥéļļö, »%1d« one two]", Pseudolocalizer.Method.ACCENT) + simpleTest("Battery %1d%%", "[βåţţéŕý »%1d«%% one two]", Pseudolocalizer.Method.ACCENT) + simpleTest("^1 %", "[^1 % one]", Pseudolocalizer.Method.ACCENT) + compoundTest("[]", Pseudolocalizer.Method.ACCENT, "", "", "") + compoundTest("[Ĥéļļö, ŵöŕļð one two]", Pseudolocalizer.Method.ACCENT, "Hello,", " world", "") + } + + @Test + fun testPlainTextBidi() { + simpleTest("", "", Pseudolocalizer.Method.BIDI) + simpleTest("word", "${bidiWordStart}word$bidiWordEnd", Pseudolocalizer.Method.BIDI) + simpleTest(" word ", " ${bidiWordStart}word$bidiWordEnd ", Pseudolocalizer.Method.BIDI) + simpleTest( + "hello\n world\n", + "${bidiWordStart}hello$bidiWordEnd\n ${bidiWordStart}world$bidiWordEnd\n", + Pseudolocalizer.Method.BIDI) + compoundTest( + "${bidiWordStart}hello$bidiWordEnd\n ${bidiWordStart}world$bidiWordEnd\n", + Pseudolocalizer.Method.BIDI, + "hello", "\n ", "world\n") + } + + @Test + fun testSimpleICU() { + // Single-fragment messages. + simpleTest("{placeholder}", "[»{placeholder}«]", Pseudolocalizer.Method.ACCENT) + simpleTest("{USER} is offline", "[»{USER}« îš öƒƒļîñé one two]", Pseudolocalizer.Method.ACCENT) + simpleTest( + "Copy from {path1} to {path2}", + "[Çöþý ƒŕöḿ »{path1}« ţö »{path2}« one two three]", + Pseudolocalizer.Method.ACCENT) + simpleTest( + "Today is {1,date} {1,time}", + "[Ţöðåý îš »{1,date}« »{1,time}« one two]", + Pseudolocalizer.Method.ACCENT) + + // Multi-fragment messages. + compoundTest( + "[»{USER}« îš öƒƒļîñé one two]", + Pseudolocalizer.Method.ACCENT, + "{USER}", " ", "is offline") + compoundTest( + "[Çöþý ƒŕöḿ »{path1}« ţö »{path2}« one two three]", + Pseudolocalizer.Method.ACCENT, + "Copy from ", "{path1}", " to", " {path2}") + } + + @Test + fun testICUBidi() { + // Single-fragment messages. + simpleTest( + "{placeholder}", "$bidiWordStart{placeholder}$bidiWordEnd", Pseudolocalizer.Method.BIDI) + simpleTest( + "{COUNT, plural, one {one} other {other}}", + "{COUNT, plural, one {${bidiWordStart}one$bidiWordEnd} " + + "other {${bidiWordStart}other$bidiWordEnd}}", + Pseudolocalizer.Method.BIDI) + } + + @Test + fun testEscaping() { + // Single-fragment messages. + simpleTest( + "'{USER'} is offline", "['{ÛŠÉŔ'} îš öƒƒļîñé one two three]", Pseudolocalizer.Method.ACCENT) + + // Multi-fragment messages. + compoundTest( + "['{ÛŠÉŔ} ''îš öƒƒļîñé one two three]", + Pseudolocalizer.Method.ACCENT, + "'{USER}", " ", "''is offline") + } + + @Test + fun testPluralsAndSelects() { + simpleTest( + "{COUNT, plural, one {Delete a file} other {Delete {COUNT} files}}", + "[{COUNT, plural, one {Ðéļéţé å ƒîļé one two} other {Ðéļéţé »{COUNT}« ƒîļéš one two}}]", + Pseudolocalizer.Method.ACCENT) + simpleTest( + "Distance is {COUNT, plural, one {# mile} other {# miles}}", + "[Ðîšţåñçé îš {COUNT, plural, one {# ḿîļé one two} other {# ḿîļéš one two}}]", + Pseudolocalizer.Method.ACCENT) + + simpleTest( + "{1, select, female {{1} added you} male {{1} added you} other {{1} added you}}", + "[{1, select, female {»{1}« åððéð ýöû one two} male {»{1}« åððéð ýöû one two} " + + "other {»{1}« åððéð ýöû one two}}]", + Pseudolocalizer.Method.ACCENT) + + compoundTest( + "[{COUNT, plural, one {Ðéļéţé å ƒîļé one two} other {Ðéļéţé »{COUNT}« ƒîļéš one two}}]", + Pseudolocalizer.Method.ACCENT, + "{COUNT, plural, one {Delete a file} ", "other {Delete ", "{COUNT}", " files}}") + } + + @Test + fun testNestedICU() { + simpleTest("{person, select, female {{num_circles, plural,=0{{person} didn't add you " + + "to any of her circles.}=1{{person} added you to one of her circles.}other{{person} added " + + "you to her # circles.}}}male {{num_circles, plural,=0{{person} didn't add you to any of " + + "his circles.}=1{{person} added you to one of his circles.}other{{person} added you to his " + + "# circles.}}}other {{num_circles, plural,=0{{person} didn't add you to any of their " + + "circles.}=1{{person} added you to one of their circles.}other{{person} added you to their " + + "# circles.}}}}", + "[{person, select, female {{num_circles, plural,=0{»{person}« ðîðñ'ţ åðð ýöû ţö åñý öƒ " + + "ĥéŕ çîŕçļéš. one two three four five}=1{»{person}« åððéð ýöû ţö öñé öƒ ĥéŕ çîŕçļéš. one " + + "two three four}other{»{person}« åððéð ýöû ţö ĥéŕ # çîŕçļéš. one two three four}}}male " + + "{{num_circles, plural,=0{»{person}« ðîðñ'ţ åðð ýöû ţö åñý öƒ ĥîš çîŕçļéš. one two three " + + "four five}=1{»{person}« åððéð ýöû ţö öñé öƒ ĥîš çîŕçļéš. one two three four}other" + + "{»{person}« åððéð ýöû ţö ĥîš # çîŕçļéš. one two three four}}}other {{num_circles, " + + "plural,=0{»{person}« ðîðñ'ţ åðð ýöû ţö åñý öƒ ţĥéîŕ çîŕçļéš. one two three four five}" + + "=1{»{person}« åððéð ýöû ţö öñé öƒ ţĥéîŕ çîŕçļéš. one two three four}other{»{person}« " + + "åððéð ýöû ţö ţĥéîŕ # çîŕçļéš. one two three four}}}}]", + Pseudolocalizer.Method.ACCENT) + } + + @Test + fun testRedefineMidLocalization() { + val localizer = Pseudolocalizer(Pseudolocalizer.Method.ACCENT) + val result = StringBuilder(localizer.text("Hello, ")) + localizer.setMethod(Pseudolocalizer.Method.NONE) + result.append(localizer.text("world!")) + Truth.assertThat(result.toString()).isEqualTo("Ĥéļļö, world!") + } + + @Test + fun testNonHtmlTags() { + simpleTest( + "Started < 1 minute ago", + "[Šţåŕţéð &ļţ; 1 ḿîñûţé åĝö one two three]", + Pseudolocalizer.Method.ACCENT) + + simpleTest( + "Started < 1 minute ago", + "[Šţåŕţéð < 1 ḿîñûţé åĝö one two three]", + Pseudolocalizer.Method.ACCENT) + + simpleTest( + "Started > 1 minute ago", + "[Šţåŕţéð > 1 ḿîñûţé åĝö one two three]", + Pseudolocalizer.Method.ACCENT) + + simpleTest( + "Started > 1 minute ago", + "[Šţåŕţéð > 1 ḿîñûţé åĝö one two three]", + Pseudolocalizer.Method.ACCENT) + + simpleTest( + "Started < 1 minute ago", + "Started < 1 minute ago", + Pseudolocalizer.Method.NONE) + + simpleTest( + "Started < 1 minute ago", + "Started < 1 minute ago", + Pseudolocalizer.Method.NONE) + + simpleTest( + "hello\n <\n world\n", + "${bidiWordStart}hello$bidiWordEnd\n " + + "${bidiWordStart}<$bidiWordEnd\n " + + "${bidiWordStart}world$bidiWordEnd\n", + Pseudolocalizer.Method.BIDI) + + simpleTest( + "hello\n <\n world\n", + "${bidiWordStart}hello$bidiWordEnd\n " + + "${bidiWordStart}<$bidiWordEnd\n " + + "${bidiWordStart}world$bidiWordEnd\n", + Pseudolocalizer.Method.BIDI) + } + + @Test + fun testHtmlTags() { + // Words should be translated, but html tags and their contents should not. + simpleTest( + "hello happy world", + "[ĥéļļö ĥåþþý ŵöŕļð one two three]", + Pseudolocalizer.Method.ACCENT) + + simpleTest( + "hello <i>happy</i> world", + "[ĥéļļö <;i>ĥåþþý<;/i> ŵöŕļð one two three]", + Pseudolocalizer.Method.ACCENT) + + // The tag (i.e. "a href="idk"") should not be translated, but the inner content (i.e."world") + // should be. + simpleTest( + "hello world", + "[ĥéļļö ŵöŕļð one two]", + Pseudolocalizer.Method.ACCENT) + + // Even if from HTML's point of view the tags are placed incorrectly, the pseudolocalizer should + // be able to handle them: not translate the tags, and only translate the contents outside and + // between them. + simpleTest( + "Incorrectly nested tags should still work", + "[Îñçöŕŕéçţļý ñéšţéð ţåĝš šĥöûļð šţîļļ ŵöŕķ one two three four five six]", + Pseudolocalizer.Method.ACCENT) + } + + +} \ No newline at end of file diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ResourceCompilerTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ResourceCompilerTest.kt new file mode 100644 index 000000000..41ef66e72 --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ResourceCompilerTest.kt @@ -0,0 +1,641 @@ +package com.android.aaptcompiler + +import com.android.aapt.Resources +import com.android.aaptcompiler.android.ResValue +import com.android.aaptcompiler.proto.deserializeConfigFromPb +import com.android.aaptcompiler.proto.deserializeFileTypeFromPb +import com.android.aaptcompiler.proto.deserializeTableFromPb +import com.android.aaptcompiler.testutils.ContainerReader +import com.android.aaptcompiler.testutils.FileEntry +import com.android.aaptcompiler.testutils.TableEntry +import com.android.aaptcompiler.testutils.parseNameOrFail +import com.android.utils.FileUtils +import com.google.common.truth.Truth +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class ResourceCompilerTest { + + @Rule + @JvmField + var tempFolder = TemporaryFolder() + + lateinit var outputDir: File + + @Before + fun setup() { + outputDir = tempFolder.newFolder() + } + + private fun createFile(input: String, name: String, parentFolder: File? = null): File { + val file = if (parentFolder != null) { + File(parentFolder, name) + } else { + tempFolder.newFile(name) + } + file.writeText(input) + return file + } + + private fun testValuesFile( + input: String, + config: String = "", + options: ResourceCompilerOptions = ResourceCompilerOptions() + ): File { + val valuesFolder = tempFolder.newFolder("values") + val configSuffix = if (config.isEmpty()) "" else "-$config" + val file = createFile(input, "test$configSuffix.xml", valuesFolder) + compileResource(file, outputDir, options, getMockBlameLogger(BlameLoggerTest.MockLogger())) + val filePath = extractPathData(file) + // The extension is changed when the table is compiled. + filePath.extension = "arsc" + return File(outputDir, filePath.getIntermediateContainerFilename()) + } + + private fun testXmlFile( + type: AaptResourceType, + input: String, + config: String = "", + options: ResourceCompilerOptions = ResourceCompilerOptions() + ): File { + val resourceFolder = tempFolder.newFolder(type.tagName) + val configSuffix = if (config.isEmpty()) "" else "-$config" + val file = createFile(input, "test$configSuffix.xml", resourceFolder) + compileResource(file, outputDir, options, getMockBlameLogger(BlameLoggerTest.MockLogger())) + val filePath = extractPathData(file) + return File(outputDir, filePath.getIntermediateContainerFilename()) + } + + private fun testRawFile(input: String, extension: String, config: String = ""): File { + val resourceFolder = tempFolder.newFolder(AaptResourceType.RAW.tagName) + val configSuffix = if (config.isEmpty()) "" else "-$config" + val file = createFile(input, "test$configSuffix.$extension", resourceFolder) + compileResource( + file, + outputDir, + ResourceCompilerOptions(), + getMockBlameLogger(BlameLoggerTest.MockLogger()) + ) + val filePath = extractPathData(file) + return File(outputDir, filePath.getIntermediateContainerFilename()) + } + + private fun testPngFile( + type: AaptResourceType, + input: String, + config: String = "", + isPatch9: Boolean = false, + options: ResourceCompilerOptions = ResourceCompilerOptions() + ): File { + val resourceFolder = tempFolder.newFolder(type.tagName) + val configSuffix = if (config.isEmpty()) "" else "-$config" + val extension = if (isPatch9) "9.png" else "png" + val file = createFile(input, "test$configSuffix.$extension", resourceFolder) + compileResource(file, outputDir, options, getMockBlameLogger(BlameLoggerTest.MockLogger())) + val filePath = extractPathData(file) + return File(outputDir, filePath.getIntermediateContainerFilename()) + } + + @Test + fun testEmptyValuesFile() { + val result = ContainerReader(testValuesFile("")) + + Truth.assertThat(result.entries).hasSize(1) + + val entry = result.entries[0] as? TableEntry + Truth.assertThat(entry).isNotNull() + + val newTable = ResourceTable() + Truth.assertThat(deserializeTableFromPb(entry!!.table, newTable, null)).isTrue() + // compilation package should still be available. + Truth.assertThat(newTable.packages).hasSize(1) + val pkg = newTable.packages[0] + Truth.assertThat(pkg.groups).isEmpty() + Truth.assertThat(pkg.name).isEqualTo("") + } + + @Test + fun failInvalidRoot() { + try { + testValuesFile("") + fail() + } catch (e: Exception) { + // expected. + } + } + + @Test + fun testRootOnlyValuesFile() { + val result = ContainerReader(testValuesFile("")) + + Truth.assertThat(result.entries).hasSize(1) + + val entry = result.entries[0] as? TableEntry + Truth.assertThat(entry).isNotNull() + + val newTable = ResourceTable() + Truth.assertThat(deserializeTableFromPb(entry!!.table, newTable, null)).isTrue() + // compilation package should still be available. + Truth.assertThat(newTable.packages).hasSize(1) + val pkg = newTable.packages[0] + Truth.assertThat(pkg.groups).isEmpty() + Truth.assertThat(pkg.name).isEqualTo("") + } + + @Test + fun testStringValuesFile() { + val input = """ + + + Hi there! + " Hello there! " + Hey + there! + Yo, how are you? + italicized and bold + Hi + + Found %d puppy! + Found %d puppies! + + + """.trimIndent() + + val result = ContainerReader(testValuesFile(input)) + Truth.assertThat(result.entries).hasSize(1) + + val entry = result.entries[0] as? TableEntry + Truth.assertThat(entry).isNotNull() + + val newTable = ResourceTable() + Truth.assertThat(deserializeTableFromPb(entry!!.table, newTable, null)).isTrue() + + val normal1 = getValue(newTable, "string/normal1") as? BasicString + Truth.assertThat(normal1).isNotNull() + Truth.assertThat(normal1!!.toString()).isEqualTo("Hi there!") + + val normal2 = getValue(newTable, "string/normal2") as? BasicString + Truth.assertThat(normal2).isNotNull() + // only spaces inside the quotes should be preserved from the xml. + Truth.assertThat(normal2!!.toString()).isEqualTo(" Hello there! ") + + val normal3 = getValue(newTable, "string/normal3") as? BasicString + Truth.assertThat(normal3).isNotNull() + // Spaces on the end are skipped for BasicStrings. However white space is compressed + // inbetween words in the strings (the newline and spaces are treated as a single space. + Truth.assertThat(normal3!!.toString()).isEqualTo("Hey there!") + + val styled1 = getValue(newTable, "string/styled1") as? StyledString + Truth.assertThat(styled1).isNotNull() + // Spaces are preserved on the ends of a styled string. + Truth.assertThat(styled1!!.toString()).isEqualTo(" Yo, how are you? ") + val spans1 = styled1.spans() + Truth.assertThat(spans1).hasSize(1) + // Spans are firstChar to lastChar inclusive, so the last char ends on 2, not 3. + Truth.assertThat(spans1[0].firstChar).isEqualTo(1) + Truth.assertThat(spans1[0].lastChar).isEqualTo(2) + Truth.assertThat(spans1[0].name.value()).isEqualTo("b") + + val styled2 = getValue(newTable, "string/styled2") as? StyledString + Truth.assertThat(styled2).isNotNull() + // Whitespace is only compressed "around" spans but not through spans. I.e. + // "b a" -> "b a" with two spaces. + Truth.assertThat(styled2!!.toString()).isEqualTo(" italicized and bold") + val spans2 = styled2.spans() + Truth.assertThat(spans2).hasSize(2) + Truth.assertThat(spans2[0].firstChar).isEqualTo(1) + Truth.assertThat(spans2[0].lastChar).isEqualTo(21) + Truth.assertThat(spans2[0].name.value()).isEqualTo("i") + Truth.assertThat(spans2[1].firstChar).isEqualTo(13) + Truth.assertThat(spans2[1].lastChar).isEqualTo(21) + Truth.assertThat(spans2[1].name.value()).isEqualTo("b") + + val styled3 = getValue(newTable, "string/styled3") as? StyledString + Truth.assertThat(styled3).isNotNull() + Truth.assertThat(styled3!!.toString()).isEqualTo("Hi") + val spans3 = styled3.spans() + Truth.assertThat(spans3).hasSize(3) + Truth.assertThat(spans3[0].firstChar).isEqualTo(0) + Truth.assertThat(spans3[0].lastChar).isEqualTo(1) + Truth.assertThat(spans3[0].name.value()).isEqualTo("all") + Truth.assertThat(spans3[1].firstChar).isEqualTo(0) + Truth.assertThat(spans3[1].lastChar).isEqualTo(1) + Truth.assertThat(spans3[1].name.value()).isEqualTo("the") + Truth.assertThat(spans3[2].firstChar).isEqualTo(0) + Truth.assertThat(spans3[2].lastChar).isEqualTo(1) + Truth.assertThat(spans3[2].name.value()).isEqualTo("spans") + + val plurals = getValue(newTable, "plurals/plural1") as? Plural + Truth.assertThat(plurals).isNotNull() + + val oneValue = plurals!!.values[Plural.Type.ONE.ordinal] as? BasicString + Truth.assertThat(oneValue).isNotNull() + Truth.assertThat(oneValue!!.toString()).isEqualTo("Found %d puppy!") + + val manyValue = plurals.values[Plural.Type.OTHER.ordinal] as? BasicString + Truth.assertThat(manyValue).isNotNull() + Truth.assertThat(manyValue!!.toString()).isEqualTo("Found %d puppies!") + + Truth.assertThat(plurals.values[Plural.Type.ZERO.ordinal]).isNull() + Truth.assertThat(plurals.values[Plural.Type.TWO.ordinal]).isNull() + Truth.assertThat(plurals.values[Plural.Type.FEW.ordinal]).isNull() + Truth.assertThat(plurals.values[Plural.Type.MANY.ordinal]).isNull() + } + + @Test + fun testParseItems() { + val input = """ + + @null + @empty + @string/nullAttempt + #abc + #f123 + #ff0000 + #ff00ffff + true + 1234 + 0x24 + 3.14 + 12dp + 100% + + """.trimIndent() + + val result = ContainerReader(testValuesFile(input)) + Truth.assertThat(result.entries).hasSize(1) + + val entry = result.entries[0] as? TableEntry + Truth.assertThat(entry).isNotNull() + + val newTable = ResourceTable() + Truth.assertThat(deserializeTableFromPb(entry!!.table, newTable, null)).isTrue() + + // Null types are empty references to aapt. + val nullReference = getValue(newTable, "string/nullAttempt") as? Reference + Truth.assertThat(nullReference).isNotNull() + Truth.assertThat(nullReference!!.name).isEqualTo(ResourceName.EMPTY) + + // Empty references are binary primitives with the null format. + val emptyReference = getValue(newTable, "string/emptyAttempt") as? BinaryPrimitive + Truth.assertThat(emptyReference).isNotNull() + Truth.assertThat(emptyReference!!.resValue.dataType).isEqualTo(ResValue.DataType.NULL) + Truth.assertThat(emptyReference.resValue.data).isEqualTo(ResValue.NullFormat.EMPTY) + + val stringReference = getValue(newTable, "string/referenceString") as? Reference + Truth.assertThat(stringReference).isNotNull() + Truth.assertThat(stringReference!!.name).isEqualTo(parseNameOrFail("string/nullAttempt")) + + val color1 = getValue(newTable, "color/color1") as? BinaryPrimitive + Truth.assertThat(color1).isNotNull() + Truth.assertThat(color1!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_COLOR_RGB4) + Truth.assertThat(color1.resValue.data).isEqualTo(0xffaabbcc.toInt()) + + val color2 = getValue(newTable, "color/color2") as? BinaryPrimitive + Truth.assertThat(color2).isNotNull() + Truth.assertThat(color2!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_COLOR_ARGB4) + Truth.assertThat(color2.resValue.data).isEqualTo(0xff112233.toInt()) + + val color3 = getValue(newTable, "color/red") as? BinaryPrimitive + Truth.assertThat(color3).isNotNull() + Truth.assertThat(color3!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_COLOR_RGB8) + Truth.assertThat(color3.resValue.data).isEqualTo(0xffff0000.toInt()) + + val color4 = getValue(newTable, "color/cyan") as? BinaryPrimitive + Truth.assertThat(color4).isNotNull() + Truth.assertThat(color4!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_COLOR_ARGB8) + Truth.assertThat(color4.resValue.data).isEqualTo(0xff00ffff.toInt()) + + val bool = getValue(newTable, "bool/check1") as? BinaryPrimitive + Truth.assertThat(bool).isNotNull() + Truth.assertThat(bool!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_BOOLEAN) + Truth.assertThat(bool.resValue.data).isEqualTo(-1) + + val integer1 = getValue(newTable, "integer/int1") as? BinaryPrimitive + Truth.assertThat(integer1).isNotNull() + Truth.assertThat(integer1!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_DEC) + Truth.assertThat(integer1.resValue.data).isEqualTo(1234) + + val integer2 = getValue(newTable, "integer/int2") as? BinaryPrimitive + Truth.assertThat(integer2).isNotNull() + Truth.assertThat(integer2!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_HEX) + Truth.assertThat(integer2.resValue.data).isEqualTo(0x24) + + val floatVal = getValue(newTable, "dimen/pi") as? BinaryPrimitive + Truth.assertThat(floatVal).isNotNull() + Truth.assertThat(floatVal!!.resValue.dataType).isEqualTo(ResValue.DataType.FLOAT) + Truth.assertThat(floatVal.resValue.data).isEqualTo(3.14.toFloat().toRawBits()) + + val dimension = getValue(newTable, "dimen/itemWidth") as? BinaryPrimitive + Truth.assertThat(dimension).isNotNull() + Truth.assertThat(dimension!!.resValue.dataType).isEqualTo(ResValue.DataType.DIMENSION) + Truth.assertThat(dimension.resValue.data).isEqualTo(0xc01) + + val fraction = getValue(newTable, "fraction/fraction1") as? BinaryPrimitive + Truth.assertThat(fraction).isNotNull() + Truth.assertThat(fraction!!.resValue.dataType).isEqualTo(ResValue.DataType.FRACTION) + Truth.assertThat(fraction.resValue.data).isEqualTo(0x100) + } + + @Test + fun testParseInvalidColorHexCode() { + val input = """ + + #0xE1E1E1 + + """.trimIndent() + + try { + testValuesFile(input) + } catch (e: ResourceCompilationException) { + Truth.assertThat(e.message).contains("Unable to parse hex color '#0xE1E1E1'.") + } + } + + @Test + fun parseStyleableWithAndroidAttr() { + val input = """ + + + + + + """.trimIndent() + + val result = ContainerReader(testValuesFile(input)) + Truth.assertThat(result.entries).hasSize(1) + + val entry = result.entries[0] as? TableEntry + Truth.assertThat(entry).isNotNull() + + val newTable = ResourceTable() + Truth.assertThat(deserializeTableFromPb(entry!!.table, newTable, null)).isTrue() + + val styleable = getValue(newTable, "styleable/oneattr") as? Styleable + Truth.assertThat(styleable).isNotNull() + Truth.assertThat(styleable!!.entries).hasSize(1) + val styleAttribute = styleable.entries[0] + Truth.assertThat(styleAttribute.name).isEqualTo(parseNameOrFail("android:attr/foo")) + } + + @Test + fun parseArrayTypes() { + val input = """ + + + 0 + 1 + + + foo + bar + + + @string/s0 + @string/s1 + @string/s2 + + + #FFFF0000 + #FF00FF00 + #FF0000FF + + + """.trimIndent() + + val result = ContainerReader(testValuesFile(input)) + Truth.assertThat(result.entries).hasSize(1) + + val entry = result.entries[0] as? TableEntry + Truth.assertThat(entry).isNotNull() + + val newTable = ResourceTable() + Truth.assertThat(deserializeTableFromPb(entry!!.table, newTable, null)).isTrue() + + val intsArray = getValue(newTable, "array/ints") as? ArrayResource + Truth.assertThat(intsArray).isNotNull() + Truth.assertThat(intsArray!!.elements).hasSize(2) + Truth.assertThat((intsArray.elements[0] as BinaryPrimitive).resValue.data).isEqualTo(0) + Truth.assertThat((intsArray.elements[1] as BinaryPrimitive).resValue.data).isEqualTo(1) + + val stringsArray1 = getValue(newTable, "array/strings") as? ArrayResource + Truth.assertThat(stringsArray1).isNotNull() + Truth.assertThat(stringsArray1!!.elements).hasSize(2) + Truth.assertThat((stringsArray1.elements[0] as BasicString).toString()).isEqualTo("foo") + Truth.assertThat((stringsArray1.elements[1] as BasicString).toString()).isEqualTo("bar") + + val stringsArray2 = getValue(newTable, "array/string_refs") as? ArrayResource + Truth.assertThat(stringsArray2).isNotNull() + Truth.assertThat(stringsArray2!!.elements).hasSize(3) + Truth.assertThat((stringsArray2.elements[0] as Reference).name) + .isEqualTo(parseNameOrFail("string/s0")) + Truth.assertThat((stringsArray2.elements[1] as Reference).name) + .isEqualTo(parseNameOrFail("string/s1")) + Truth.assertThat((stringsArray2.elements[2] as Reference).name) + .isEqualTo(parseNameOrFail("string/s2")) + + val colorsArray = getValue(newTable, "array/colors") as? ArrayResource + Truth.assertThat(colorsArray).isNotNull() + Truth.assertThat(colorsArray!!.elements).hasSize(3) + Truth.assertThat((colorsArray.elements[0] as? BinaryPrimitive)?.resValue?.data) + .isEqualTo(0xffff0000.toInt()) + Truth.assertThat((colorsArray.elements[1] as? BinaryPrimitive)?.resValue?.data) + .isEqualTo(0xff00ff00.toInt()) + Truth.assertThat((colorsArray.elements[2] as? BinaryPrimitive)?.resValue?.data) + .isEqualTo(0xff0000ff.toInt()) + } + + @Test + fun parseAttrInStyleable() { + val input = """ + + + + + + + + + + """.trimIndent() + + val result = ContainerReader(testValuesFile(input)) + Truth.assertThat(result.entries).hasSize(1) + + val entry = result.entries[0] as? TableEntry + Truth.assertThat(entry).isNotNull() + + val newTable = ResourceTable() + Truth.assertThat(deserializeTableFromPb(entry!!.table, newTable, null)).isTrue() + + val styleable = getValue(newTable, "styleable/PieChart") as? Styleable + Truth.assertThat(styleable).isNotNull() + Truth.assertThat(styleable!!.entries).hasSize(2) + + val reference1 = styleable.entries[0] + Truth.assertThat(reference1.name).isEqualTo(parseNameOrFail("attr/showText")) + + val reference2 = styleable.entries[1] + Truth.assertThat(reference2.name).isEqualTo(parseNameOrFail("attr/labelPosition")) + + val attr1 = getValue(newTable, "attr/showText") as? AttributeResource + Truth.assertThat(attr1).isNotNull() + Truth.assertThat(attr1!!.symbols).hasSize(0) + + val attr2 = getValue(newTable, "attr/labelPosition") as? AttributeResource + Truth.assertThat(attr2).isNotNull() + Truth.assertThat(attr2!!.symbols).hasSize(2) + + val symbol1 = attr2.symbols[0] + Truth.assertThat(symbol1.type).isEqualTo(ResValue.DataType.INT_DEC.byteValue) + Truth.assertThat(symbol1.value).isEqualTo(0) + Truth.assertThat(symbol1.symbol.name).isEqualTo(parseNameOrFail("id/left")) + + val symbol2 = attr2.symbols[1] + Truth.assertThat(symbol2.type).isEqualTo(ResValue.DataType.INT_DEC.byteValue) + Truth.assertThat(symbol2.value).isEqualTo(1) + Truth.assertThat(symbol2.symbol.name).isEqualTo(parseNameOrFail("id/right")) + } + + @Test + fun testCompileRawFileSupported() { + val input = """ + I'm just writing some text here, because raw files can be anything. + """.trimIndent() + + val result = ContainerReader(testRawFile(input, "txt")) + Truth.assertThat(result.numEntries).isEqualTo(1) + + val entry = result.entries[0] as? FileEntry + Truth.assertThat(entry).isNotNull() + + val header = entry!!.header + val data = entry.data + + Truth.assertThat(header.resourceName).isEqualTo("raw/test") + Truth.assertThat(deserializeConfigFromPb(header.config, null).toString()) + .isEqualTo("DEFAULT") + Truth.assertThat(header.exportedSymbolCount).isEqualTo(0) + Truth.assertThat(header.type).isEqualTo(Resources.FileReference.Type.UNKNOWN) + + val dataString = String(data) + Truth.assertThat(dataString).isEqualTo(input) + } + + @Test + fun testCompilePatch9ProcessingNotSupported() { + val input = """ + What is here doesn't really matter, as the files location and extension determines + whether or not this file is treated as a png. + """.trimIndent() + + try { + testPngFile(AaptResourceType.LAYOUT, input, isPatch9 = true) + fail() + } catch (e: ResourceCompilationException) { + Truth.assertThat(e.cause!!.message).contains( + "Patch 9 PNG processing is not supported with the JVM Android resource compiler." + ) + // expected + } + } + + @Test + fun testCompilePngCrunchingNotSupported() { + val input = """ + What is here doesn't really matter, as the files location and extension determines + whether or not this file is treated as a png. + """.trimIndent() + + try { + testPngFile( + AaptResourceType.LAYOUT, + input, + options = ResourceCompilerOptions(requirePngCrunching = true) + ) + fail() + } catch (e: ResourceCompilationException) { + Truth.assertThat(e.cause!!.message).contains( + "PNG crunching is not supported with the JVM Android resource compiler." + ) + // expected + } + } + + @Test + fun testCompilePngWithoutCrunchingSupported() { + val input = """ + What is here doesn't really matter, as the files location and extension determines + whether or not this file is treated as a png. + """.trimIndent() + + val result = ContainerReader(testPngFile(AaptResourceType.LAYOUT, input)) + Truth.assertThat(result.numEntries).isEqualTo(1) + + val entry = result.entries[0] as? FileEntry + Truth.assertThat(entry).isNotNull() + + val header = entry!!.header + val data = entry.data + + Truth.assertThat(header.resourceName).isEqualTo("layout/test") + Truth.assertThat(deserializeConfigFromPb(header.config, null).toString()) + .isEqualTo("DEFAULT") + Truth.assertThat(header.exportedSymbolCount).isEqualTo(0) + Truth.assertThat(header.type).isEqualTo(Resources.FileReference.Type.UNKNOWN) + + val dataString = String(data) + Truth.assertThat(dataString).isEqualTo(input) + } + + @Test + fun testCompileFileWithCustomSourcePath() { + val input = // language=XML + """ + + + + """.trimIndent() + + val path = FileUtils.join("main", "res", "values", "test.xml") + val options = ResourceCompilerOptions(sourcePath = path) + val result = ContainerReader( + testXmlFile(type = AaptResourceType.LAYOUT, input = input, options = options)) + Truth.assertThat(result.numEntries).isEqualTo(1) + val entries = result.entries as List + val sourcePath: String = entries[0].header.sourcePath + Truth.assertThat(sourcePath).isEqualTo(path) + } + + @Test + fun testMacros() { + val input = """ + + + Hello world + @macro/m1 + + """.trimIndent() + + val result = ContainerReader(testValuesFile(input)) + Truth.assertThat(result.entries).hasSize(1) + + val entry = result.entries[0] as? TableEntry + Truth.assertThat(entry).isNotNull() + + val newTable = ResourceTable() + Truth.assertThat(deserializeTableFromPb(entry!!.table, newTable, null)).isTrue() + + val m1 = getValue(newTable, "macro/m1") as? Macro + Truth.assertThat(m1).isNotNull() + Truth.assertThat(m1!!.rawValue).isEqualTo("Hello world") + + val m2 = getValue(newTable, "macro/m2") as? Macro + Truth.assertThat(m2).isNotNull() + Truth.assertThat(m2!!.rawValue).isEqualTo("@macro/m1") + } +} diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ResourceTableTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ResourceTableTest.kt new file mode 100644 index 000000000..e4d8ccc94 --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ResourceTableTest.kt @@ -0,0 +1,479 @@ +package com.android.aaptcompiler + +import com.android.aapt.Resources +import com.android.aaptcompiler.android.ResTableConfig +import com.android.aaptcompiler.android.ResValue +import com.itsaky.androidide.layoutlib.resources.ResourceVisibility +import com.google.common.truth.Truth +import org.junit.Test + +internal fun getValue( + table: ResourceTable, + resName: String, + config: ConfigDescription = ConfigDescription(), + productName: String = ""): Value? { + + val name = parseResourceName(resName)!!.resourceName + + val result = table.findResource(name) + result ?: return null + + val configValue = result.entry.findValue(config, productName) + configValue ?: return null + + return configValue.value +} + +class ResourceTableTest { + + @Test + fun failToAddResourceWithBadName() { + val table = ResourceTable() + + val id = Id() + id.source = Source("test.xml", 21) + + Truth.assertThat( + table.addResource( + ResourceName("android", AaptResourceType.ID, "hey,there"), + ConfigDescription(), + "", + id)).isFalse() + + Truth.assertThat( + table.addResource( + ResourceName("android", AaptResourceType.ID, "hey:there"), + ConfigDescription(), + "", + id)).isFalse() + } + + @Test + fun testAddResourceWithMangledNameHandlesWeirdNames() { + val table = ResourceTable() + val id = Id() + id.source = Source ("test.xml", 23) + + Truth.assertThat( + table.addResourceMangled( + parseResourceName("android:id/heythere ")!!.resourceName, + ConfigDescription(), + "", + id)).isTrue() + } + + @Test + fun testAddResource() { + val table = ResourceTable() + + val id = Id() + id.source = Source("test.xml", 25) + + Truth.assertThat( + table.addResource( + parseResourceName("android:attr/id")!!.resourceName, + ConfigDescription(), + "", + id)).isTrue() + + Truth.assertThat(getValue(table, "android:attr/id")).isNotNull() + } + + @Test + fun testStyleables() { + // Styleable children are nested deep under ResourceEntry, so the ResourceGroup.getStyleable() + // method was introduce. This test checks that it works correctly. + val table = ResourceTable() + + val styleableOne = Styleable() + styleableOne.entries.add(0, Reference(ResourceName("", AaptResourceType.ATTR, "child_one"))) + styleableOne.entries.add(1, Reference(ResourceName("android", AaptResourceType.ATTR, "child_two"))) + + Truth.assertThat( + table.addResource( + ResourceName("", AaptResourceType.STYLEABLE, "styleable_parent_one"), + ConfigDescription(), + "", + styleableOne)).isTrue() + + val styleableTwo = Styleable() + styleableTwo.entries.add(0, Reference(ResourceName("", AaptResourceType.ATTR, "child_one"))) + styleableTwo.entries.add(1, Reference(ResourceName("android", AaptResourceType.ATTR, "child_three"))) + + Truth.assertThat( + table.addResource( + ResourceName("", AaptResourceType.STYLEABLE, "styleable_parent_two"), + ConfigDescription(), + "", + styleableTwo)).isTrue() + + Truth.assertThat(getValue(table, "styleable/styleable_parent_one")).isNotNull() + Truth.assertThat(getValue(table, "styleable/styleable_parent_two")).isNotNull() + + Truth.assertThat(table.packages).hasSize(1) + + Truth.assertThat(table.packages[0].groups).hasSize(1) + val styleableGroup = table.packages[0].groups[0] + + Truth.assertThat(styleableGroup.entries).hasSize(2) + + val styleableEntryOne = styleableGroup.entries.entries.toList()[0] + Truth.assertThat(styleableEntryOne).isNotNull() + Truth.assertThat(styleableEntryOne.value.values.first().name).isEqualTo("styleable_parent_one") + val children = table.packages[0].groups[0].getStyleable(styleableEntryOne).entries + Truth.assertThat(children).hasSize(2) + Truth.assertThat(children[0].name.toString()).isEqualTo("attr/child_one") + Truth.assertThat(children[1].name.toString()).isEqualTo("android:attr/child_two") + + val styleableEntryTwo = styleableGroup.entries.entries.toList()[1] + Truth.assertThat(styleableEntryTwo).isNotNull() + Truth.assertThat(styleableEntryTwo.value.values.first().name).isEqualTo("styleable_parent_two") + val children_two = table.packages[0].groups[0].getStyleable(styleableEntryTwo).entries + Truth.assertThat(children_two).hasSize(2) + Truth.assertThat(children_two[0].name.toString()).isEqualTo("attr/child_one") + Truth.assertThat(children_two[1].name.toString()).isEqualTo("android:attr/child_three") + } + + @Test + fun testAddMultipleResource() { + val table = ResourceTable() + val config = ConfigDescription() + val languageConfig = + ConfigDescription(ResTableConfig(language = byteArrayOf('p'.toByte(), 'l'.toByte()))) + + val id1 = Id() + id1.source = Source("test/path/file.xml", 10) + val id2 = Id() + id2.source = Source("test/path/file.xml", 12) + val id3 = Id() + id3.source = Source("test/path/file.xml", 14) + val id4 = Id() + id4.source = Source ("test/path/file.xml", 20) + + Truth.assertThat( + table.addResource( + parseResourceName("android:attr/layout_width")!!.resourceName, + config, + "", + id1)).isTrue() + + Truth.assertThat( + table.addResource( + parseResourceName("android:attr/id")!!.resourceName, + config, + "", + id2)).isTrue() + + Truth.assertThat( + table.addResource( + parseResourceName("android:string/ok")!!.resourceName, + config, + "", + id3)).isTrue() + + Truth.assertThat( + table.addResource( + parseResourceName("android:string/ok")!!.resourceName, + languageConfig, + "", + id4)).isTrue() + + Truth.assertThat(getValue(table, "android:attr/layout_width")).isNotNull() + Truth.assertThat(getValue(table, "android:attr/id")).isNotNull() + Truth.assertThat(getValue(table, "android:string/ok")).isNotNull() + Truth.assertThat(getValue(table, "android:string/ok", languageConfig)).isNotNull() + } + + @Test + fun testOverrideWeakResource() { + val table = ResourceTable() + val config = ConfigDescription() + + val weakAttr = AttributeResource() + weakAttr.weak = true + val strongAttr = AttributeResource() + strongAttr.weak = false + + Truth.assertThat( + table.addResource( + parseResourceName("android:attr/foo")!!.resourceName, + config, + "", + weakAttr)).isTrue() + + var attr = getValue(table, "android:attr/foo") as? AttributeResource + Truth.assertThat(attr).isNotNull() + Truth.assertThat(attr!!.weak).isTrue() + + Truth.assertThat( + table.addResource( + parseResourceName("android:attr/foo")!!.resourceName, + config, + "", + strongAttr)).isTrue() + + attr = getValue(table, "android:attr/foo") as? AttributeResource + Truth.assertThat(attr).isNotNull() + Truth.assertThat(attr!!.weak).isFalse() + } + + @Test + fun testCompatibleDuplicateAttributesAreAllowed() { + val table = ResourceTable() + val config = ConfigDescription() + + val name = parseResourceName("android:attr/foo")!!.resourceName + val weak1 = AttributeResource(Resources.Attribute.FormatFlags.STRING_VALUE) + weak1.weak = true + val weak2 = AttributeResource( + Resources.Attribute.FormatFlags.STRING_VALUE or + Resources.Attribute.FormatFlags.REFERENCE_VALUE) + weak2.weak = true + + Truth.assertThat(table.addResource(name, config, "", weak1)).isTrue() + Truth.assertThat(table.addResource(name, config, "", weak2)).isTrue() + } + + @Test + fun testProductVaryingValues() { + val table = ResourceTable() + val config = parse("land") + + val name = parseResourceName("android:string/foo")!!.resourceName + + Truth.assertThat(table.addResource(name, config, "tablet", Id())).isTrue() + Truth.assertThat(table.addResource(name, config, "phone", Id())).isTrue() + + Truth.assertThat(getValue(table, "android:string/foo", parse("land"), "tablet")).isNotNull() + Truth.assertThat(getValue(table, "android:string/foo", parse("land"), "phone")).isNotNull() + + val searchResult = table.findResource(parseResourceName("android:string/foo")!!.resourceName) + Truth.assertThat(searchResult).isNotNull() + + val values = searchResult!!.entry.values + values.sortBy { it.product } + Truth.assertThat(values).hasSize(2) + Truth.assertThat(values[0].product).isEqualTo("phone") + Truth.assertThat(values[1].product).isEqualTo("tablet") + } + + private fun visibilityOfResourceTest( + table: ResourceTable, + name: ResourceName, + expected: ResourceVisibility, + expectedComment: String) { + + val result = table.findResource(name) + Truth.assertThat(result).isNotNull() + + val visibility = result!!.entry.visibility + Truth.assertThat(visibility.level).isEqualTo(expected) + Truth.assertThat(visibility.comment).isEqualTo(expectedComment) + } + + @Test + fun testSetVisibility() { + val table = ResourceTable() + val name = parseResourceName("android:string/foo")!!.resourceName + + var visibility = Visibility(comment = "private", level = ResourceVisibility.PRIVATE) + Truth.assertThat(table.setVisibility(name, visibility)).isTrue() + visibilityOfResourceTest(table, name, ResourceVisibility.PRIVATE, "private") + + // Undefined visibility will not override defined visibility. + visibility = Visibility(comment = "undefined", level = ResourceVisibility.UNDEFINED) + Truth.assertThat(table.setVisibility(name, visibility)).isTrue() + visibilityOfResourceTest(table, name, ResourceVisibility.PRIVATE, "private") + + // Public visibility clashes with previously defined private visibility. + visibility = Visibility(comment = "public", level = ResourceVisibility.PUBLIC) + Truth.assertThat(table.setVisibility(name, visibility)).isFalse() + + } + + @Test + fun testSetVisibilityWithId() { + val table = ResourceTable() + + var name = parseResourceName("android:string/foo")!!.resourceName + val visibility1 = Visibility(comment = "default", level = ResourceVisibility.UNDEFINED) + Truth.assertThat(table.setVisibilityWithId(name, visibility1, 0)).isTrue() + visibilityOfResourceTest(table, name, ResourceVisibility.UNDEFINED, "default") + + val visibility2 = Visibility(comment = "public", level = ResourceVisibility.PUBLIC) + Truth.assertThat(table.setVisibilityWithId(name, visibility2, 0x01020000)).isTrue() + visibilityOfResourceTest(table, name, ResourceVisibility.PUBLIC, "public") + + name = parseResourceName("android:string/foobar")!!.resourceName + val visibility3 = Visibility(comment = "private2", level = ResourceVisibility.PRIVATE) + Truth.assertThat(table.setVisibilityWithId(name, visibility3, 0)).isTrue() + visibilityOfResourceTest(table, name, ResourceVisibility.PRIVATE, "private2") + } + + @Test + fun testSetAllowNew() { + val table = ResourceTable() + val name = parseResourceName("android:string/foo")!!.resourceName + + var allowNew = AllowNew(Source(""), "first") + Truth.assertThat(table.setAllowNew(name, allowNew)).isTrue() + var result = table.findResource(name) + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.entry.allowNew).isNotNull() + Truth.assertThat(result.entry.allowNew!!.comment).isEqualTo("first") + + allowNew = AllowNew(Source(""), "second") + Truth.assertThat(table.setAllowNew(name, allowNew)).isTrue() + result = table.findResource(name) + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.entry.allowNew).isNotNull() + Truth.assertThat(result.entry.allowNew!!.comment).isEqualTo("second") + } + + @Test + fun testSetOverlayable() { + val table = ResourceTable() + val overlayable = + Overlayable("Name", "overlay://theme", Source("res/values/overlayable.xml", 40)) + + val overlayableItem = OverlayableItem( + overlayable, + OverlayableItem.Policy.PRODUCT or OverlayableItem.Policy.VENDOR, + "comment", + Source("res/values/overlayable.xml", 42)) + + val name = parseResourceName("android:string/foo")!!.resourceName + table.setOverlayable(name, overlayableItem) + + val result = table.findResource(name) + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.entry.overlayable).isNotNull() + + val entryOverlayable = result.entry.overlayable!! + Truth.assertThat(entryOverlayable.overlayable.name).isEqualTo("Name") + Truth.assertThat(entryOverlayable.overlayable.actor).isEqualTo("overlay://theme") + Truth.assertThat(entryOverlayable.overlayable.source.path) + .isEqualTo("res/values/overlayable.xml") + Truth.assertThat(entryOverlayable.overlayable.source.line).isEqualTo(40) + Truth.assertThat(entryOverlayable.policies) + .isEqualTo(OverlayableItem.Policy.PRODUCT or OverlayableItem.Policy.VENDOR) + Truth.assertThat(entryOverlayable.comment).isEqualTo("comment") + Truth.assertThat(entryOverlayable.source.path).isEqualTo("res/values/overlayable.xml") + Truth.assertThat(entryOverlayable.source.line).isEqualTo(42) + } + + @Test + fun testSetMultipleOverlayableResources() { + val table = ResourceTable() + + val foo = parseResourceName("android:string/foo")!!.resourceName + val overlayable = Overlayable("Name", "overlay://theme", Source("")) + val overlayableItem = OverlayableItem(overlayable, OverlayableItem.Policy.PRODUCT) + Truth.assertThat(table.setOverlayable(foo, overlayableItem)).isTrue() + + val bar = parseResourceName("android:string/bar")!!.resourceName + val overlayableItem2 = OverlayableItem(overlayable, OverlayableItem.Policy.PRODUCT) + Truth.assertThat(table.setOverlayable(bar, overlayableItem2)).isTrue() + + val baz = parseResourceName("android:string/baz")!!.resourceName + val overlayableItem3 = OverlayableItem(overlayable, OverlayableItem.Policy.VENDOR) + Truth.assertThat(table.setOverlayable(baz, overlayableItem3)).isTrue() + } + + @Test + fun testSetSameOverlayableDifferentNames() { + val table = ResourceTable() + + val foo = parseResourceName("android:string/foo")!!.resourceName + val overlayable = Overlayable("Name", "overlay://theme", Source("")) + val overlayableItem = OverlayableItem(overlayable, OverlayableItem.Policy.PRODUCT) + Truth.assertThat(table.setOverlayable(foo, overlayableItem)).isTrue() + + val bar = parseResourceName("android:string/bar")!!.resourceName + val overlayable2 = Overlayable("Name2", "overlay://theme", Source("")) + val overlayableItem2 = OverlayableItem(overlayable2, OverlayableItem.Policy.PRODUCT) + Truth.assertThat(table.setOverlayable(bar, overlayableItem2)).isTrue() + } + + @Test + fun failToSetOverlayablesSameResource() { + val table = ResourceTable() + + val name = parseResourceName("android:string/foo")!!.resourceName + + val overlayable = Overlayable("Name", "overlay://theme", Source("")) + val overlayableItem = OverlayableItem(overlayable) + Truth.assertThat(table.setOverlayable(name, overlayableItem)).isTrue() + + val overlayableItem2 = OverlayableItem(overlayable) + Truth.assertThat(table.setOverlayable(name, overlayableItem2)).isFalse() + } + + @Test + fun failToSetDifferentOverlayablesSameResource() { + val table = ResourceTable() + + val name = parseResourceName("android:string/foo")!!.resourceName + + val overlayable = Overlayable("Name", "overlay://theme", Source("")) + val overlayableItem = OverlayableItem(overlayable) + Truth.assertThat(table.setOverlayable(name, overlayableItem)).isTrue() + + val overlayable2 = Overlayable("Other", "overlay://theme", Source("")) + val overlayableItem2 = OverlayableItem(overlayable2) + Truth.assertThat(table.setOverlayable(name, overlayableItem2)).isFalse() + } + + @Test + fun testAllowDuplicateResourceNames() { + val table = ResourceTable(false) + + val name = parseResourceName("android:bool/foo")!!.resourceName + + Truth.assertThat( + table.addResourceWithId( + name, + 0x7f0100ff, + ConfigDescription(), + "", + BinaryPrimitive(ResValue(ResValue.DataType.INT_BOOLEAN, 0)))).isTrue() + Truth.assertThat( + table.addResourceWithId( + name, + 0x7f010100, + ConfigDescription(), + "", + BinaryPrimitive(ResValue(ResValue.DataType.INT_BOOLEAN, 1)))).isTrue() + + Truth.assertThat( + table.setVisibilityWithId( + name, Visibility(level = ResourceVisibility.PUBLIC), 0x7f0100ff)).isTrue() + + Truth.assertThat( + table.setVisibilityWithId( + name, Visibility(level = ResourceVisibility.PRIVATE), 0x7f010100)).isTrue() + + val tablePackage = table.findPackageById(0x7f) + Truth.assertThat(tablePackage).isNotNull() + val tableGroup = tablePackage!!.findGroup(AaptResourceType.BOOL) + Truth.assertThat(tableGroup).isNotNull() + + val entry1 = tableGroup!!.findEntry("foo", 0x00ff) + Truth.assertThat(entry1).isNotNull() + Truth.assertThat(entry1!!.id).isEqualTo(0x00ff) + Truth.assertThat(entry1.values).hasSize(1) + val value1 = entry1.values[0].value as? BinaryPrimitive + Truth.assertThat(value1).isNotNull() + Truth.assertThat(value1!!.resValue.data).isEqualTo(0) + Truth.assertThat(entry1.visibility.level).isEqualTo(ResourceVisibility.PUBLIC) + + val entry2 = tableGroup.findEntry("foo", 0x0100) + Truth.assertThat(entry2).isNotNull() + Truth.assertThat(entry2!!.id).isEqualTo(0x0100) + Truth.assertThat(entry2.values).hasSize(1) + val value2 = entry2.values[0].value as? BinaryPrimitive + Truth.assertThat(value2).isNotNull() + Truth.assertThat(value2!!.resValue.data).isEqualTo(1) + Truth.assertThat(entry2.visibility.level).isEqualTo(ResourceVisibility.PRIVATE) + } +} diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ResourceUtilsTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ResourceUtilsTest.kt new file mode 100644 index 000000000..a503389bf --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/ResourceUtilsTest.kt @@ -0,0 +1,156 @@ +package com.android.aaptcompiler + +import com.android.aapt.Resources +import com.android.aaptcompiler.android.ResValue +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ResourceUtilsTest { + + @Test + fun testParseBool() { + assertThat(parseAsBool("true")).isTrue() + assertThat(parseAsBool("TRUE")).isTrue() + assertThat(parseAsBool("True")).isTrue() + + assertThat(parseAsBool("false")).isFalse() + assertThat(parseAsBool("FALSE")).isFalse() + assertThat(parseAsBool("False")).isFalse() + + assertThat(parseAsBool(" False\n ")).isFalse() + } + + @Test + fun testParseResourceName() { + var result = parseResourceName("android:color/foo") + assertThat(result).isNotNull() + assertThat(result?.resourceName) + .isEqualTo(ResourceName("android", AaptResourceType.COLOR, "foo")) + assertThat(result?.isPrivate).isFalse() + + result = parseResourceName("color/foo") + assertThat(result).isNotNull() + assertThat(result?.resourceName) + .isEqualTo(ResourceName("", AaptResourceType.COLOR, "foo")) + assertThat(result?.isPrivate).isFalse() + + result = parseResourceName("*android:string/foo") + assertThat(result).isNotNull() + assertThat(result?.resourceName) + .isEqualTo(ResourceName("android", AaptResourceType.STRING, "foo")) + assertThat(result?.isPrivate).isTrue() + + result = parseResourceName("") + assertThat(result).isNull() + } + + @Test + fun testParseReferenceWithNoPackage() { + val result = parseReference("@color/bar") + assertThat(result).isNotNull() + assertThat(result?.reference?.name) + .isEqualTo(ResourceName("", AaptResourceType.COLOR, "bar")) + assertThat(result?.createNew).isFalse() + assertThat(result?.reference?.isPrivate).isFalse() + } + + @Test + fun testParseReferenceWithPackage() { + val result = parseReference("@android:integer/foo") + assertThat(result).isNotNull() + assertThat(result?.reference?.name) + .isEqualTo(ResourceName("android", AaptResourceType.INTEGER, "foo")) + assertThat(result?.createNew).isFalse() + assertThat(result?.reference?.isPrivate).isFalse() + } + + @Test + fun testParseReferenceWithSurroundingWhitespace() { + val result = parseReference("\t @android:integer/foo\n \n\t") + assertThat(result).isNotNull() + assertThat(result?.reference?.name) + .isEqualTo(ResourceName("android", AaptResourceType.INTEGER, "foo")) + assertThat(result?.createNew).isFalse() + assertThat(result?.reference?.isPrivate).isFalse() + } + + @Test + fun testParseAutoCreateIdReference() { + val result = parseReference("@+android:id/foo") + assertThat(result).isNotNull() + assertThat(result?.reference?.name) + .isEqualTo(ResourceName("android", AaptResourceType.ID, "foo")) + assertThat(result?.createNew).isTrue() + assertThat(result?.reference?.isPrivate).isFalse() + } + + @Test + fun testParsePrivateReference() { + val result = parseReference("@*android:id/foo") + assertThat(result).isNotNull() + assertThat(result?.reference?.name) + .isEqualTo(ResourceName("android", AaptResourceType.ID, "foo")) + assertThat(result?.createNew).isFalse() + assertThat(result?.reference?.isPrivate).isTrue() + } + + @Test + fun testFailToParseAutoCreateNonIdReference() { + val result = parseReference("@+android:color/foo") + assertThat(result).isNull() + } + + @Test + fun testParseAttributeReference() { + assertThat(parseAttributeReference("?android")).isNotNull() + assertThat(parseAttributeReference("?android:foo")).isNotNull() + assertThat(parseAttributeReference("?attr/foo")).isNotNull() + assertThat(parseAttributeReference("?android:attr/foo")).isNotNull() + + assertThat(parseAttributeReference("?style/foo")).isNull() + assertThat(parseAttributeReference("?android:style/foo")).isNull() + assertThat(parseAttributeReference("?android:")).isNull() + assertThat(parseAttributeReference("?android:attr/")).isNull() + assertThat(parseAttributeReference("?:attr/")).isNull() + assertThat(parseAttributeReference("?:attr/foo")).isNull() + assertThat(parseAttributeReference("?:/")).isNull() + assertThat(parseAttributeReference("?:/foo")).isNull() + assertThat(parseAttributeReference("?attr/")).isNull() + assertThat(parseAttributeReference("?/foo")).isNull() + } + + @Test + fun testNullIsEmptyReference() { + assertThat(makeNull()).isEqualTo(Reference()) + assertThat(tryParseNullOrEmpty("@null")).isEqualTo(Reference()) + } + + @Test + fun testEmptyIsBinaryPrimitive() { + assertThat(makeEmpty()) + .isEqualTo(BinaryPrimitive(ResValue(ResValue.DataType.NULL, ResValue.NullFormat.EMPTY))) + assertThat(tryParseNullOrEmpty("@empty")) + .isEqualTo(BinaryPrimitive(ResValue(ResValue.DataType.NULL, ResValue.NullFormat.EMPTY))) + } + + @Test + fun testItemsWithWhitespaceAreParsedCorrectly() { + var result = + tryParseItemForAttribute(" 12\n ", Resources.Attribute.FormatFlags.INTEGER_VALUE) + assertThat(result).isNotNull() + assertThat(result) + .isEqualTo(BinaryPrimitive(ResValue(ResValue.DataType.INT_DEC, 12))) + result = + tryParseItemForAttribute(" true\n ", Resources.Attribute.FormatFlags.BOOLEAN_VALUE) + assertThat(result).isNotNull() + assertThat(result) + .isEqualTo(BinaryPrimitive(ResValue(ResValue.DataType.INT_BOOLEAN, -1))) + + val expectedFloat = 12.0f + result = + tryParseItemForAttribute(" 12.0\n ", Resources.Attribute.FormatFlags.FLOAT_VALUE) + assertThat(result).isNotNull() + assertThat(result) + .isEqualTo(BinaryPrimitive(ResValue(ResValue.DataType.FLOAT, expectedFloat.toRawBits()))) + } +} \ No newline at end of file diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/StringPoolTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/StringPoolTest.kt new file mode 100644 index 000000000..07f49666c --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/StringPoolTest.kt @@ -0,0 +1,334 @@ +package com.android.aaptcompiler + +import com.android.aaptcompiler.android.ResStringPool +import com.android.aaptcompiler.buffer.BigBuffer +import com.google.common.truth.Truth +import org.junit.Assert.assertThrows +import org.junit.Ignore +import org.junit.Test +import java.nio.ByteBuffer + +class StringPoolTest { + + @Test + fun testInsertOneString() { + val pool = StringPool() + + val ref = pool.makeRef("wut") + Truth.assertThat(ref.value()).isEqualTo("wut") + } + + @Test + fun testInsertTwoStrings() { + val pool = StringPool() + + val ref1 = pool.makeRef("wut") + val ref2 = pool.makeRef("hey") + + Truth.assertThat(ref1.value()).isEqualTo("wut") + Truth.assertThat(ref2.value()).isEqualTo("hey") + } + + @Test + fun testInsertDuplicateStrings() { + val pool = StringPool() + + val ref1 = pool.makeRef("wut") + val ref2 = pool.makeRef("wut") + + Truth.assertThat(ref1.value()).isEqualTo("wut") + Truth.assertThat(ref2.value()).isEqualTo("wut") + Truth.assertThat(pool.size()).isEqualTo(1) + } + + @Test + fun testStringsWithDifferentPriorityAreNotDuplicates() { + val pool = StringPool() + + val ref1 = pool.makeRef("wut", StringPool.Context(0x81010001L)) + val ref2 = pool.makeRef("wut", StringPool.Context(0x81010002L)) + + Truth.assertThat(ref1.value()).isEqualTo("wut") + Truth.assertThat(ref2.value()).isEqualTo("wut") + Truth.assertThat(pool.size()).isEqualTo(2) + } + + @Test + fun testMaintainInsertionOrderIndex() { + val pool = StringPool() + + val ref1 = pool.makeRef("z") + val ref2 = pool.makeRef("a") + val ref3 = pool.makeRef("m") + + Truth.assertThat(ref1.index()).isEqualTo(0) + Truth.assertThat(ref2.index()).isEqualTo(1) + Truth.assertThat(ref3.index()).isEqualTo(2) + } + + @Test + @Ignore("b/303074426") // TODO(@daniellabar): fix pruning and reference management. + fun pruneStringsWithNoReferences() { + // TEST HERE + } + + @Test + fun sortAndMaintainIndexesInStringReferences() { + val pool = StringPool() + + val ref1 = pool.makeRef("z") + val ref2 = pool.makeRef("a") + val ref3 = pool.makeRef("m") + + Truth.assertThat(ref1.value()).isEqualTo("z") + Truth.assertThat(ref1.index()).isEqualTo(0) + + Truth.assertThat(ref2.value()).isEqualTo("a") + Truth.assertThat(ref2.index()).isEqualTo(1) + + Truth.assertThat(ref3.value()).isEqualTo("m") + Truth.assertThat(ref3.index()).isEqualTo(2) + + pool.sort() + + Truth.assertThat(ref1.value()).isEqualTo("z") + Truth.assertThat(ref1.index()).isEqualTo(2) + + Truth.assertThat(ref2.value()).isEqualTo("a") + Truth.assertThat(ref2.index()).isEqualTo(0) + + Truth.assertThat(ref3.value()).isEqualTo("m") + Truth.assertThat(ref3.index()).isEqualTo(1) + } + + @Test + fun deduplicateStillWorksWithSort() { + val pool = StringPool() + + val ref1 = pool.makeRef("z") + val ref2 = pool.makeRef("a") + val ref3 = pool.makeRef("m") + + pool.sort() + + val ref4 = pool.makeRef("z") + val ref5 = pool.makeRef("a") + val ref6 = pool.makeRef("m") + + Truth.assertThat(ref1.index()).isEqualTo(ref4.index()) + Truth.assertThat(ref2.index()).isEqualTo(ref5.index()) + Truth.assertThat(ref3.index()).isEqualTo(ref6.index()) + } + + @Test + fun testAddStyles() { + val pool = StringPool() + + val ref = pool.makeRef(StyleString("android", listOf(Span("b", 2, 6)))) + Truth.assertThat(ref.index()).isEqualTo(0) + Truth.assertThat(ref.value()).isEqualTo("android") + Truth.assertThat(ref.spans()).hasSize(1) + + val span = ref.spans()[0] + Truth.assertThat(span.name.value()).isEqualTo("b") + Truth.assertThat(span.firstChar).isEqualTo(2) + Truth.assertThat(span.lastChar).isEqualTo(6) + } + + @Test + fun doNotDeduplicateStyleWithString() { + val pool = StringPool() + + val ref = pool.makeRef("android") + val styleRef = pool.makeRef(StyleString("android", listOf())) + + Truth.assertThat(ref.index()).isNotEqualTo(styleRef.index()) + } + + @Test + fun testSortingKeepsStylesAndStringsSeparate() { + val pool = StringPool() + + val ref1 = pool.makeRef(StyleString("beta", listOf())) + val ref2 = pool.makeRef("alpha") + val ref3 = pool.makeRef(StyleString("alpha", listOf())) + + Truth.assertThat(ref2.index()).isNotEqualTo(ref3.index()) + + pool.sort() + + Truth.assertThat(ref3.index()).isEqualTo(0) + Truth.assertThat(ref1.index()).isEqualTo(1) + Truth.assertThat(ref2.index()).isEqualTo(2) + } + + @Test + fun flattenEmptyStringPoolUtf8() { + val pool = StringPool() + + val buffer = BigBuffer(1028) + + pool.flattenUtf8(buffer, null) + Truth.assertThat(buffer.size).isEqualTo(28) + + val test = ResStringPool.get(ByteBuffer.wrap(buffer.toBytes()), buffer.size) + Truth.assertThat(test.stringPoolSize).isEqualTo(0) + } + + @Test + fun flattenOddCharactersUtf16() { + val pool = StringPool() + pool.makeRef("\u093f") + + val buffer = BigBuffer(1028) + + pool.flattenUtf16(buffer, null) + // header size (28) + string array (4) + string length (2) + string itself (2) + + // null terminator (2) + padding (2) + Truth.assertThat(buffer.size).isEqualTo(40) + + val test = ResStringPool.get(ByteBuffer.wrap(buffer.toBytes()), buffer.size) + Truth.assertThat(test.strings).hasSize(1) + val string = test.strings[0] + Truth.assertThat(string).isEqualTo("\u093f") + } + + @Test + fun flattenPool() { + val pool = StringPool() + + val ref1 = pool.makeRef("hello") + val ref2 = pool.makeRef("goodbye") + val ref3 = pool.makeRef(LONG_STRING) + val ref4 = pool.makeRef("") + val ref5 = pool.makeRef(StyleString("style", listOf(Span("b", 0, 1), Span("i", 2, 3)))) + + Truth.assertThat(ref5.index()).isEqualTo(0) + + Truth.assertThat(ref1.index()).isEqualTo(1) + Truth.assertThat(ref2.index()).isEqualTo(2) + Truth.assertThat(ref3.index()).isEqualTo(3) + Truth.assertThat(ref4.index()).isEqualTo(4) + + val utf8Buffer = BigBuffer(1024) + val utf16Buffer = BigBuffer(1024) + + pool.flattenUtf8(utf8Buffer, null) + pool.flattenUtf16(utf16Buffer, null) + + // Test both buffers. + val utf8Test = ResStringPool.get(ByteBuffer.wrap(utf8Buffer.toBytes()), utf8Buffer.size) + val utf16Test = ResStringPool.get(ByteBuffer.wrap(utf16Buffer.toBytes()), utf16Buffer.size) + + // size = number of direct string refs (5) + number of refs created by spans. + Truth.assertThat(utf8Test.strings).hasSize(7) + Truth.assertThat(utf16Test.strings).hasSize(7) + + Truth.assertThat(utf8Test.strings[0]).isEqualTo("style") + Truth.assertThat(utf16Test.strings[0]).isEqualTo("style") + + Truth.assertThat(utf8Test.strings[1]).isEqualTo("hello") + Truth.assertThat(utf16Test.strings[1]).isEqualTo("hello") + + Truth.assertThat(utf8Test.strings[2]).isEqualTo("goodbye") + Truth.assertThat(utf16Test.strings[2]).isEqualTo("goodbye") + + Truth.assertThat(utf8Test.strings[3]).isEqualTo(LONG_STRING) + Truth.assertThat(utf16Test.strings[3]).isEqualTo(LONG_STRING) + + Truth.assertThat(utf8Test.strings[4]).isEqualTo("") + Truth.assertThat(utf16Test.strings[4]).isEqualTo("") + + Truth.assertThat(utf8Test.styles).hasSize(1) + Truth.assertThat(utf16Test.styles).hasSize(1) + + val utf8Style = utf8Test.styles[0] + val utf16Style = utf16Test.styles[0] + + Truth.assertThat(utf8Test.strings[utf8Style[0].name.index]).isEqualTo("b") + Truth.assertThat(utf16Test.strings[utf16Style[0].name.index]).isEqualTo("b") + + Truth.assertThat(utf8Style[0].firstChar).isEqualTo(0) + Truth.assertThat(utf16Style[0].firstChar).isEqualTo(0) + + Truth.assertThat(utf8Style[0].lastChar).isEqualTo(1) + Truth.assertThat(utf16Style[0].lastChar).isEqualTo(1) + + Truth.assertThat(utf8Test.strings[utf8Style[1].name.index]).isEqualTo("i") + Truth.assertThat(utf16Test.strings[utf16Style[1].name.index]).isEqualTo("i") + + Truth.assertThat(utf8Style[1].firstChar).isEqualTo(2) + Truth.assertThat(utf16Style[1].firstChar).isEqualTo(2) + + Truth.assertThat(utf8Style[1].lastChar).isEqualTo(3) + Truth.assertThat(utf16Style[1].lastChar).isEqualTo(3) + } + + @Test + fun testMaxEncodingLengthUTF8() { + val pool1 = StringPool() + pool1.makeRef("Why, hello!") + var utf8Buffer = BigBuffer(1024) + pool1.flattenUtf8(utf8Buffer, null) + val utf8Test = ResStringPool.get(ByteBuffer.wrap(utf8Buffer.toBytes()), utf8Buffer.size) + + Truth.assertThat(utf8Test.strings).hasSize(1) + Truth.assertThat(utf8Test.strings[0]).isEqualTo("Why, hello!") + + val pool2 = StringPool() + val superLongString = String(CharArray(33000) {'a'}) + + pool2.makeRef("This fits1") + pool2.makeRef(superLongString) + pool2.makeRef("This fits2") + + utf8Buffer = BigBuffer(1024) + val utf8Exception = assertThrows(Exception::class.java) { + pool2.flattenUtf8(utf8Buffer, null) + } + + Truth.assertThat(utf8Exception.message).contains( + "String of size 33000 bytes is too large to encode using UTF-8 (32767 bytes).") + } + + @Test + fun testMaxEncodingLengthUTF16() { + val pool1 = StringPool() + + pool1.makeRef("Why, hello!") + + var utf16Buffer = BigBuffer(1024) + + pool1.flattenUtf16(utf16Buffer, null) + + var utf16Test = ResStringPool.get(ByteBuffer.wrap(utf16Buffer.toBytes()), utf16Buffer.size) + + Truth.assertThat(utf16Test.strings).hasSize(1) + + Truth.assertThat(utf16Test.strings[0]).isEqualTo("Why, hello!") + + val pool2 = StringPool() + val superLongString = String(CharArray(33000) {'a'}) + + pool2.makeRef("This fits1") + pool2.makeRef(superLongString) + pool2.makeRef("This fits2") + + utf16Buffer = BigBuffer(1024) + pool2.flattenUtf16(utf16Buffer, null) + utf16Test = ResStringPool.get(ByteBuffer.wrap(utf16Buffer.toBytes()), utf16Buffer.size) + + Truth.assertThat(utf16Test.strings).hasSize(3) + Truth.assertThat(utf16Test.strings[0]).isEqualTo("This fits1") + Truth.assertThat(utf16Test.strings[1]).isEqualTo(superLongString) + Truth.assertThat(utf16Test.strings[2]).isEqualTo("This fits2") + } + + companion object { + const val LONG_STRING = "バッテリーを長持ちさせるため、バッテリーセーバーは端末のパフォーマンスを抑" + + "え、バイブレーション、位置情報サービス、大半のバックグラウンドデータを制限" + + "します。メール、SMSや、同期を使 " + + "用するその他のアプリは、起動しても更新されないことがあります。バッテリーセ" + + "ーバーは端末の充電中は自動的にOFFになります。" + } +} diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/TableExtractorTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/TableExtractorTest.kt new file mode 100644 index 000000000..c7e6feabc --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/TableExtractorTest.kt @@ -0,0 +1,1364 @@ +package com.android.aaptcompiler + +import com.android.aaptcompiler.android.ResValue +import com.android.aapt.Resources +import com.itsaky.androidide.layoutlib.resources.ResourceVisibility +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test + +const val XML_PREAMBLE = "\n" + +class TableExtractorTest { + + lateinit var table: ResourceTable + + @Before + fun setup() { + table = ResourceTable() + } + + private fun testParse( + input: String, + config: ConfigDescription = ConfigDescription(), + mockLogger: BlameLoggerTest.MockLogger = BlameLoggerTest.MockLogger() + ): Boolean { + val parseInput = + """$XML_PREAMBLE + + $input + + """.trimIndent() + + val blameLogger = getMockBlameLogger(mockLogger) + + val extractor = + TableExtractor(table, Source("test.xml"), config, TableExtractorOptions(), blameLogger) + + try { + extractor.extract(parseInput.byteInputStream()) + } catch (e: Exception) { + return false + } + return true + } + + private fun getValue( + resName: String, + config: ConfigDescription = ConfigDescription(), + productName: String = "") = + getValue(table, resName, config, productName) + + @Test + fun testParseEmptyInput() { + val input = "$XML_PREAMBLE\n" + + val mockLogger = BlameLoggerTest.MockLogger() + + val extractor = + TableExtractor( + table, + Source("test.xml"), + ConfigDescription(), + TableExtractorOptions(), + getMockBlameLogger(mockLogger)) + + extractor.extract(input.byteInputStream()) + assertThat(mockLogger.errors).isEmpty() + assertThat(mockLogger.warnings).isEmpty() + } + + @Test + fun failToParseWithNoRoot() { + val input = """$XML_PREAMBLE""" + val mockLogger = BlameLoggerTest.MockLogger() + val blameLogger = getMockBlameLogger(mockLogger) + val extractor = + TableExtractor( + table, Source("test.xml"), ConfigDescription(), TableExtractorOptions(), blameLogger) + + val extractorException = assertThrows(Exception::class.java) { + extractor.extract(input.byteInputStream()) + } + assertThat(extractorException).isNotNull() + assertThat(extractorException.message) + .contains("Root xml element of resource table not labeled 'resources' (test.xml.rewritten:0:1:).") + } + + @Test + fun failToParseDuplicates() { + val input = """ + + + + + """.trimIndent() + + val mockLogger = BlameLoggerTest.MockLogger() + assertThat(testParse(input, mockLogger = mockLogger)).isFalse() + assertThat(mockLogger.errors).hasSize(1) + val errorMsg = mockLogger.errors.single().first + + assertThat(errorMsg).contains( + "test.xml.rewritten:7:1: Duplicate symbol 'id/bar' defined here:") + assertThat(errorMsg).contains( + "test.xml.rewritten:7:1: and here:") + assertThat(errorMsg) + .contains("test.xml.rewritten:6:1") + } + + @Test + fun testParseBoolean() { + val input = """ + true + false""".trimIndent() + + assertThat(testParse(input)).isTrue() + + val boolA = getValue("bool/a") as? BinaryPrimitive + assertThat(boolA).isNotNull() + assertThat(boolA!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_BOOLEAN) + assertThat(boolA.resValue.data).isEqualTo(-1) + + val boolB = getValue("bool/b") as? BinaryPrimitive + assertThat(boolB).isNotNull() + assertThat(boolB!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_BOOLEAN) + assertThat(boolB.resValue.data).isEqualTo(0) + } + + @Test + fun testParseColor() { + val input = """ + #7fa87f + @android:color/black + """.trimIndent() + + assertThat(testParse(input)).isTrue() + + val colorA = getValue("color/a") as? BinaryPrimitive + assertThat(colorA).isNotNull() + assertThat(colorA!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_COLOR_RGB8) + assertThat(colorA.resValue.data).isEqualTo(0xff7fa87f.toInt()) + + val colorB = getValue("color/b") as? Reference + assertThat(colorB).isNotNull() + assertThat(colorB!!.name) + .isEqualTo(parseResourceName("android:color/black")!!.resourceName) + } + + @Test + fun parseDimen() { + val input = + """ + 16dp + @dimen/abc_control_padding_material + 10% + """.trimIndent() + + assertThat(testParse(input)).isTrue() + + val dimenA = getValue("dimen/a") as? BinaryPrimitive + assertThat(dimenA).isNotNull() + assertThat(dimenA!!.resValue.dataType).isEqualTo(ResValue.DataType.DIMENSION) + assertThat(dimenA.resValue.data).isEqualTo(0x1001) + + val dimenB = getValue("dimen/b") as? Reference + assertThat(dimenB).isNotNull() + assertThat(dimenB!!.name) + .isEqualTo(parseResourceName("dimen/abc_control_padding_material")!!.resourceName) + + val dimenC = getValue("dimen/c") as? BinaryPrimitive + assertThat(dimenC).isNotNull() + assertThat(dimenC!!.resValue.dataType).isEqualTo(ResValue.DataType.FRACTION) + // 0 00011001100110011001101 0011 0000 + // ^ positive ^ 10% or .1 with rounding. ^ Radix 0p23 (all fraction) ^ non-parent fraction + // i.e. 0x0ccccd30 + assertThat(dimenC.resValue.data).isEqualTo(0x0ccccd30) + } + + @Test + fun testParseDrawable() { + assertThat(testParse("""#0cffffff""")).isTrue() + + val drawable = getValue("drawable/foo") as? BinaryPrimitive + assertThat(drawable).isNotNull() + assertThat(drawable!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_COLOR_ARGB8) + assertThat(drawable.resValue.data).isEqualTo(0xcffffff) + } + + @Test + fun testParseInteger() { + val input = """ + 10 + 0x10 + 0xA + """.trimIndent() + + assertThat(testParse(input)).isTrue() + + val integerA = getValue("integer/a") as? BinaryPrimitive + assertThat(integerA).isNotNull() + assertThat(integerA!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_DEC) + assertThat(integerA.resValue.data).isEqualTo(10) + + val integerB = getValue("integer/b") as? BinaryPrimitive + assertThat(integerB).isNotNull() + assertThat(integerB!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_HEX) + assertThat(integerB.resValue.data).isEqualTo(16) + + val integerC = getValue("integer/c") as? BinaryPrimitive + assertThat(integerC).isNotNull() + assertThat(integerC!!.resValue.dataType).isEqualTo(ResValue.DataType.INT_HEX) + assertThat(integerC.resValue.data).isEqualTo(10) + } + + @Test + fun testParseId() { + assertThat(testParse("""""")).isTrue() + assertThat(getValue("id/foo") as? Id).isNotNull() + } + + @Test + fun testParsingNonItemId() { + assertThat(testParse("""""")).isTrue() + assertThat(getValue("id/foo") as? Id).isNotNull() + } + + @Test + fun testParseQuotedString() { + + assertThat(testParse(""" " hey there " """)).isTrue() + var str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEqualTo(" hey there ") + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse("""Isn\'t it cool?""")).isTrue() + str = getValue("string/bar") as BasicString + assertThat(str.toString()).isEqualTo("Isn't it cool?") + + assertThat(testParse(""""Isn't it cool?"""")).isTrue() + str = getValue("string/baz") as BasicString + assertThat(str.toString()).isEqualTo("Isn't it cool?") + } + + @Test + fun testParseCDataString() { + assertThat(testParse("""""")).isTrue() + var str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEqualTo("basic") + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse(""" + Try span]]>""")).isTrue() + str = getValue("string/bar") as BasicString + assertThat(str.toString()).isEqualTo("Try span") + + // Testing multiple CDATA spans and whitespace behavior across spans. + assertThat(testParse(""" + trial]]>multiple trials]]> + """.trimIndent())).isTrue() + str = getValue("string/baz") as BasicString + assertThat(str.toString()).isEqualTo("trialmultiple trials") + + // Quotes are handled as expected. + assertThat(testParse(""" + " " + """.trimIndent())) + str = getValue("string/bat") as BasicString + assertThat(str.toString()).isEqualTo(" Let's go! ") + + // Quotes are handle the same inside or out of CDATA. + assertThat(testParse(""" + + """.trimIndent())).isTrue() + str = getValue("string/bat2") as BasicString + assertThat(str.toString()).isEqualTo(" Let's go! ") + + // Or across the CDATA border. + assertThat(testParse(""" + " " + """.trimIndent())).isTrue() + str = getValue("string/bat3") as BasicString + assertThat(str.toString()).isEqualTo(" Let's go! ") + + // Invalid xml in CDATA is okay. + assertThat(testParse(""" + xml]]> + """.trimIndent())).isTrue() + str = getValue("string/bax") as BasicString + assertThat(str.toString()).isEqualTo("xml") + + assertThat(testParse(""" + + """.trimIndent())).isTrue() + str = getValue("string/bav") as BasicString + assertThat(str.toString()).isEqualTo("\" QUOTE TIME \"") + } + + @Test + fun testParseEscapedString() { + assertThat(testParse("""\?123""")).isTrue() + var str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEqualTo("?123") + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse("""This isn\'t a bad string""")) + str = getValue("string/bar") as BasicString + assertThat(str.toString()).isEqualTo("This isn't a bad string") + } + + @Test + fun testParseFormattedString() { + val mockLogger = BlameLoggerTest.MockLogger() + assertThat( + testParse("""%1${"$"}d %2${"$"}s""", mockLogger = mockLogger)) + .isTrue() + assertThat(mockLogger.errors).isEmpty() + assertThat(mockLogger.warnings).isEmpty() + + assertThat( + testParse("""%d %s""", mockLogger = mockLogger)).isFalse() + assertThat(mockLogger.errors).hasSize(1) + assertThat(mockLogger.errors.single().first).contains( + "test.xml.rewritten:5:1: Multiple substitutions specified in non-positional format of " + + "string resource string/foo. ") + assertThat(mockLogger.warnings).isEmpty() + + } + + @Test + fun testParseUnformattedString() { + assertThat(testParse("""%10""")).isTrue() + } + + @Test + fun testNonAsciiString() { + assertThat(testParse("""डिफ़ॉल्ट""")).isTrue() + } + + @Test + fun testParseStyledString() { + val input = + "This is my aunt\u2019s fickle string" + assertThat(testParse(input)).isTrue() + + val str = getValue("string/foo") as StyledString + + assertThat(str.toString()).isEqualTo("This is my aunt\u2019s fickle string") + assertThat(str.spans()).hasSize(2) + assertThat(str.untranslatableSections).isEmpty() + + val span0 = str.spans()[0] + assertThat(span0.name.value()).isEqualTo("b") + assertThat(span0.firstChar).isEqualTo(18) + assertThat(span0.lastChar).isEqualTo(30) + + val span1 = str.spans()[1] + assertThat(span1.name.value()).isEqualTo("small") + assertThat(span1.firstChar).isEqualTo(25) + assertThat(span1.lastChar).isEqualTo(30) + } + + @Test + fun testParseStringWithWhitespace() { + assertThat(testParse(""" This is what I think """)) + + var str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEqualTo("This is what I think") + assertThat(str.untranslatables).isEmpty() + + assertThat( + testParse("""" This is what I think """")) + + str = getValue("string/foo2") as BasicString + assertThat(str.toString()).isEqualTo(" This is what I think ") + assertThat(str.untranslatables).isEmpty() + + assertThat( + testParse(""""\nHello world\n\n"""")) + + str = getValue("string/foo3") as BasicString + assertThat(str.toString()).isEqualTo("\nHello world\n\n") + assertThat(str.untranslatables).isEmpty() + } + + @Test + fun testParseStringWithExplicitNewLines() { + assertThat(testParse("""\n\nTest\n\n""")) + + var str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEqualTo("\n\nTest\n\n") + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse(""" \n """)) + + str = getValue("string/foo2") as BasicString + assertThat(str.toString()).isEqualTo("\n") + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse("""\n\n\n\n\n\n\n\n\n""")) + + str = getValue("string/foo3") as BasicString + assertThat(str.toString()).isEqualTo("\n\n\n\n\n\n\n\n\n") + assertThat(str.untranslatables).isEmpty() + } + + @Test + fun testParseStringWithBasicLineBreaking() { + assertThat(testParse(""" + + + Hello + world + + + """.trimMargin())) + + val str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEqualTo("Hello world") + assertThat(str.untranslatables).isEmpty() + } + + @Test + fun testStringOfWhitespaces() { + assertThat(testParse(""" """)) + + var str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEmpty() + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse(""" + + + + + + """.trimMargin())) + + str = getValue("string/foo2") as BasicString + assertThat(str.toString()).isEmpty() + assertThat(str.untranslatables).isEmpty() + } + + @Test + fun testEmptyString() { + assertThat(testParse("""""")) + + val str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEmpty() + assertThat(str.untranslatables).isEmpty() + } + + @Test + fun testShortStrings() { + assertThat(testParse("""a""")) + + var str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEqualTo("a") + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse("""\n""")) + + str = getValue("string/foo2") as BasicString + assertThat(str.toString()).isEqualTo("\n") + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse(""""""")) + + str = getValue("string/foo3") as BasicString + //AAPT2 removes the single quotes, so should be empty. + assertThat(str.toString()).isEmpty() + assertThat(str.untranslatables).isEmpty() + } + + @Test + fun testStringWithSlash() { + assertThat(testParse("""\""")) + + var str = getValue("string/foo") as BasicString + assertThat(str.toString()).isEqualTo("") + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse("""\woo\""")) + + str = getValue("string/foo2") as BasicString + assertThat(str.toString()).isEqualTo("woo") + assertThat(str.untranslatables).isEmpty() + + assertThat(testParse("""\@\'\\""")) + + str = getValue("string/foo3") as BasicString + assertThat(str.toString()).isEqualTo("@'\\") + assertThat(str.untranslatables).isEmpty() + } + + @Test + fun testIgnoreXliffTagsOtherThanG() { + val input = """ + + There are no apples + """.trimIndent() + assertThat(testParse(input)).isTrue() + + var str = getValue("string/foo") as? BasicString + assertThat(str).isNotNull() + str = str!! + assertThat(str.toString()).isEqualTo("There are no apples") + assertThat(str.untranslatables).isEmpty() + } + + @Test + fun failToParseNestedXliffGTags() { + val input = """ + + Do not translate this + """.trimIndent() + assertThat(testParse(input)).isFalse() + } + + @Test + fun testParseUntranslatableSections() { + val input = """ + + There are %1${"$"}d apples + """.trimIndent() + assertThat(testParse(input)).isTrue() + + val str = getValue("string/foo") as? BasicString + assertThat(str).isNotNull() + str!! + assertThat(str.toString()).isEqualTo("There are %1\$d apples") + assertThat(str.untranslatables).hasSize(1) + + val untranslatables0 = str.untranslatables[0] + assertThat(untranslatables0.startIndex).isEqualTo(10) + assertThat(untranslatables0.endIndex).isEqualTo(14) + } + + @Test + fun testParseUntranslatablesInStyledString() { + val input = """ + + There are %1${"$"}d apples + """.trimIndent() + assertThat(testParse(input)).isTrue() + + val str = getValue("string/foo") as? StyledString + assertThat(str).isNotNull() + str!! + assertThat(str.toString()).isEqualTo(" There are %1\$d apples") + assertThat(str.spans()).hasSize(1) + assertThat(str.untranslatableSections).hasSize(1) + + val untranslatables0 = str.untranslatableSections[0] + assertThat(untranslatables0.startIndex).isEqualTo(11) + assertThat(untranslatables0.endIndex).isEqualTo(15) + + val span0 = str.spans()[0] + assertThat(span0.name.value()).isEqualTo("b") + assertThat(span0.firstChar).isEqualTo(11) + assertThat(span0.lastChar).isEqualTo(14) + } + + @Test + fun testParseNull() { + val input = """@null""" + assertThat(testParse(input)).isTrue() + + // The Android runtime treats a value of android::Res_value::TYPE_NULL as a non-existing value, + // and this causes problems in styles when trying to resolve an attribute. Null values must be + // encoded as android::Res_value::TYPE_REFERENCE with a data value of 0. + val nullRef = getValue("integer/foo") as? Reference + assertThat(nullRef).isNotNull() + nullRef!! + assertThat(nullRef.name).isEqualTo(ResourceName("", AaptResourceType.RAW, "")) + assertThat(nullRef.id).isNull() + assertThat(nullRef.referenceType).isEqualTo(Reference.Type.RESOURCE) + } + + @Test + fun testParseEmptyValue() { + val input = """@empty""" + assertThat(testParse(input)).isTrue() + + val integer = getValue("integer/foo") as? BinaryPrimitive + assertThat(integer).isNotNull() + integer!! + assertThat(integer.resValue.dataType).isEqualTo(ResValue.DataType.NULL) + assertThat(integer.resValue.data).isEqualTo(ResValue.NullFormat.EMPTY) + } + + @Test + fun testParseAttr() { + val input = """ + + + """.trimIndent() + assertThat(testParse(input)).isTrue() + + val attr1 = getValue("attr/foo") as? AttributeResource + assertThat(attr1).isNotNull() + attr1!! + assertThat(attr1.typeMask).isEqualTo(Resources.Attribute.FormatFlags.STRING_VALUE) + + val attr2 = getValue("attr/bar") as? AttributeResource + assertThat(attr2).isNotNull() + attr2!! + assertThat(attr2.typeMask).isEqualTo(Resources.Attribute.FormatFlags.ANY_VALUE) + } + + // Old AAPT allowed attributes to be defined under different configurations, but ultimately + // stored them with the default configuration. Check that we have the same behavior. + @Test + fun testParseAttrAndDeclareStyleableUnderConfigButRecordAsNoConfig() { + val watchConfig = parse("watch") + val input = """ + + + + + """.trimIndent() + assertThat(testParse(input, watchConfig)).isTrue() + + assertThat(getValue("attr/foo", watchConfig)).isNull() + assertThat(getValue("attr/baz", watchConfig)).isNull() + assertThat(getValue("styleable/bar", watchConfig)).isNull() + + assertThat(getValue("attr/foo")).isNotNull() + assertThat(getValue("attr/baz")).isNotNull() + assertThat(getValue("styleable/bar")).isNotNull() + + val styleable = table.packages[0].groups[1].entries.asIterable().first() + assertThat(styleable).isNotNull() + + val styleableItem = table.packages[0].groups[1].getStyleable(styleable) + assertThat(styleableItem).isNotNull() + assertThat(styleableItem.entries.size).isEqualTo(1) + assertThat(styleableItem.entries[0].name.entry).isEqualTo("baz") + } + + @Test + fun testEmptyStyleable() { + val input = """ + + """.trimIndent() + assertThat(testParse(input)).isTrue() + assertThat(getValue("styleable/bar")).isNotNull() + + assertThat(table.packages.size).isEqualTo(1) + val pck = table.packages[0] + + assertThat(pck.groups.size).isEqualTo(1) + val group = pck.groups[0] + + assertThat(group.entries.size).isEqualTo(1) + group.entries.forEach { + assertThat(it.key).isEqualTo("bar") + assertThat(group.type.tagName).isEqualTo("styleable") + val styleableContainer = it.value.values.first().values + assertThat(styleableContainer.size).isEqualTo(1) + assertThat(styleableContainer[0].value).isInstanceOf(Styleable::class.java) + assertThat(styleableContainer[0].value).isEqualTo(group.getStyleable(it)) + } + } + + @Test + fun testParseAttrWithMinMax() { + val input = """""" + assertThat(testParse(input)).isTrue() + + val attr = getValue("attr/foo") as? AttributeResource + assertThat(attr).isNotNull() + attr!! + assertThat(attr.typeMask).isEqualTo(Resources.Attribute.FormatFlags.INTEGER_VALUE) + assertThat(attr.minInt).isEqualTo(10) + assertThat(attr.maxInt).isEqualTo(23) + } + + @Test + fun failToParseAttrWithMinMaxButNotInteger() { + val input = """""" + assertThat(testParse(input)).isFalse() + } + + @Test + fun testParseUseAndDeclarationOfAttr() { + val input = """ + + + + + """.trimIndent() + assertThat(testParse(input)).isTrue() + + val attr = getValue("attr/foo") as? AttributeResource + assertThat(attr).isNotNull() + attr!! + assertThat(attr.typeMask).isEqualTo(Resources.Attribute.FormatFlags.STRING_VALUE) + } + + @Test + fun testParseDoubleUseOfAttr() { + val input = """ + + + + + + + """.trimIndent() + assertThat(testParse(input)).isTrue() + + val attr = getValue("attr/foo") as? AttributeResource + assertThat(attr).isNotNull() + attr!! + assertThat(attr.typeMask).isEqualTo(Resources.Attribute.FormatFlags.BOOLEAN_VALUE) + } + + @Test + fun testParseEnumAttr() { + val input = """ + + + + + + """.trimIndent() + assertThat(testParse(input)).isTrue() + + val attr = getValue("attr/foo") as? AttributeResource + assertThat(attr).isNotNull() + attr!! + assertThat(attr.typeMask).isEqualTo(Resources.Attribute.FormatFlags.ENUM_VALUE) + assertThat(attr.symbols).hasSize(3) + + val symbol0 = attr.symbols[0] + assertThat(symbol0.symbol.name.entry).isEqualTo("bar") + assertThat(symbol0.value).isEqualTo(0) + + val symbol1 = attr.symbols[1] + assertThat(symbol1.symbol.name.entry).isEqualTo("bat") + assertThat(symbol1.value).isEqualTo(1) + + val symbol2 = attr.symbols[2] + assertThat(symbol2.symbol.name.entry).isEqualTo("baz") + assertThat(symbol2.value).isEqualTo(2) + } + + @Test + fun testParseFlagAttr() { + val input = """ + + + + + + """.trimIndent() + assertThat(testParse(input)).isTrue() + + val attr = getValue("attr/foo") as? AttributeResource + assertThat(attr).isNotNull() + attr!! + assertThat(attr.typeMask).isEqualTo(Resources.Attribute.FormatFlags.FLAGS_VALUE) + assertThat(attr.symbols).hasSize(3) + + val symbol0 = attr.symbols[0] + assertThat(symbol0.symbol.name.entry).isEqualTo("bar") + assertThat(symbol0.value).isEqualTo(0) + + val symbol1 = attr.symbols[1] + assertThat(symbol1.symbol.name.entry).isEqualTo("bat") + assertThat(symbol1.value).isEqualTo(1) + + val symbol2 = attr.symbols[2] + assertThat(symbol2.symbol.name.entry).isEqualTo("baz") + assertThat(symbol2.value).isEqualTo(2) + + val flagValue = tryParseFlagSymbol(attr, "baz | bat") + assertThat(flagValue).isNotNull() + flagValue!! + assertThat(flagValue.resValue.data).isEqualTo(1 or 2) + } + + @Test + fun failParseEnumNonUniqueKeys() { + val input = """ + + + + + + """.trimIndent() + assertThat(testParse(input)).isFalse() + } + + @Test + fun testParseStyle() { + val input = """ + + """.trimIndent() + assertThat(testParse(input)).isTrue() + + val style = getValue("style/foo") as? Style + assertThat(style).isNotNull() + style!! + assertThat(style.parent).isNotNull() + assertThat(style.parent!!.name) + .isEqualTo(parseResourceName("style/fu")!!.resourceName) + assertThat(style.entries).hasSize(3) + + assertThat(style.entries[0].key.name) + .isEqualTo(parseResourceName("attr/bar")!!.resourceName) + assertThat(style.entries[1].key.name) + .isEqualTo(parseResourceName("attr/bat")!!.resourceName) + assertThat(style.entries[2].key.name) + .isEqualTo(parseResourceName("attr/baz")!!.resourceName) + } + + @Test + fun testParseStyleWithShorthandParent() { + assertThat(testParse(""" + """.trimIndent() + assertThat(testParse(input)).isTrue() + + val style = getValue("style/foo") as? Style + assertThat(style).isNotNull() + style!! + assertThat(style.entries).hasSize(1) + assertThat(style.entries[0].key.name) + .isEqualTo(parseResourceName("android:attr/bar")!!.resourceName) + } + + @Test + fun testParseStyleWithInferredParent() { + assertThat(testParse("""""" + assertThat(testParse(sample)).isTrue() + val style = getValue("style/Widget.ImageButton.Custom") as Style + val item = style.entries[0].value as RawString + assertThat(item.value.value()).isEqualTo("some value") + } + + @Test + fun testParseItemElementWithValueOnNewLineWithNoQuotations() { + val sample = + """""" + assertThat(testParse(sample)).isTrue() + val style = getValue("style/Widget.ImageButton.Custom") as Style + val item = style.entries[0].value as RawString + assertThat(item.value.value()).isEqualTo("some value") + } + + @Test + fun testMacro() { + val mockLogger = BlameLoggerTest.MockLogger() + + val macros = """ + @string/foo + @integer/foo + ?attr/foo + @macro/foo + Hello world + 123 + """.trimIndent() + + val result = testParse(macros, mockLogger = mockLogger) + assertThat(result).isTrue() + assertThat(mockLogger.errors).isEmpty() + assertThat(mockLogger.warnings).isEmpty() + + val stringRefMacro = getValue("macro/m_string") as Macro + assertThat(stringRefMacro).isNotNull() + assertThat(stringRefMacro.rawValue).isEqualTo("@string/foo") + + val intRefMacro = getValue("macro/m_int") as Macro + assertThat(intRefMacro).isNotNull() + assertThat(intRefMacro.rawValue).isEqualTo("@integer/foo") + + val maybeAttrRefMacro = getValue("macro/m_attr") as Macro + assertThat(maybeAttrRefMacro).isNotNull() + assertThat(maybeAttrRefMacro.rawValue).isEqualTo("?attr/foo") + + val macroRefMacro = getValue("macro/m_macro") as Macro + assertThat(macroRefMacro).isNotNull() + assertThat(macroRefMacro.rawValue).isEqualTo("@macro/foo") + + val rawStringValueMacro = getValue("macro/m_text_value") as Macro + assertThat(rawStringValueMacro).isNotNull() + assertThat(rawStringValueMacro.untranslatables).isEmpty() + assertThat(rawStringValueMacro.rawValue).isEqualTo("Hello world") + + // Any text that isn't a reference is held as a string in a macro + val rawIntValueMacro = getValue("macro/m_int_value") as Macro + assertThat(rawIntValueMacro).isNotNull() + assertThat(rawIntValueMacro.untranslatables).isEmpty() + assertThat(rawIntValueMacro.rawValue).isEqualTo("123") + } + + @Test + fun testMacroMissingName() { + val mockLogger = BlameLoggerTest.MockLogger() + + val result = testParse("", mockLogger = mockLogger) + assertThat(result).isFalse() + assertThat(mockLogger.errors).hasSize(1) + assertThat(mockLogger.errors.single().first).contains(" is missing the 'name' attribute.") + } + +} diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/UnicodeUtilTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/UnicodeUtilTest.kt new file mode 100644 index 000000000..5d5992b67 --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/UnicodeUtilTest.kt @@ -0,0 +1,60 @@ +package com.android.aaptcompiler + +import com.google.common.truth.Truth +import org.junit.Test + +class UnicodeUtilTest { + + @Test + fun testIsXidStart() { + val validInput = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZˮø" + for (i in 0.until(validInput.codePointCount(0, validInput.length))) { + Truth.assertThat(isXidStart(validInput.codePointAt(i))).isTrue() + } + + val invalidInput = "$;\'/<>+=-.{}[]()\\|?@#%^&*!~`\",1234567890_" + for (i in 0.until(invalidInput.codePointCount(0, invalidInput.length))) { + Truth.assertThat(isXidStart(invalidInput.codePointAt(i))).isFalse() + } + } + + @Test + fun testIsXidContinue() { + val validInput = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_ˮø" + for (i in 0.until(validInput.codePointCount(0, validInput.length))) { + Truth.assertThat(isXidContinue(validInput.codePointAt(i))).isTrue() + } + + val invalidInput = "$;\'/<>+=-.{}[]()\\|?@#%^&*!~`\"," + for (i in 0.until(invalidInput.codePointCount(0, invalidInput.length))) { + Truth.assertThat(isXidContinue(invalidInput.codePointAt(i))).isFalse() + } + } + + @Test + fun testIsJavaIdentifier() { + Truth.assertThat(isJavaIdentifier("FøøBar_12")).isTrue() + Truth.assertThat(isJavaIdentifier("Føø\$Bar")).isTrue() + Truth.assertThat(isJavaIdentifier("_FøøBar")).isTrue() + Truth.assertThat(isJavaIdentifier("\$Føø\$Bar")).isTrue() + + Truth.assertThat(isJavaIdentifier("12FøøBar")).isFalse() + Truth.assertThat(isJavaIdentifier(".Hello")).isFalse() + } + + @Test + fun testIsValidResourceEntryName() { + Truth.assertThat(isJavaIdentifier("FøøBar")).isTrue() + Truth.assertThat(isValidResourceEntryName("FøøBar_12")).isTrue() + Truth.assertThat(isValidResourceEntryName("Føø.Bar")).isTrue() + Truth.assertThat(isValidResourceEntryName("Føø-Bar")).isTrue() + Truth.assertThat(isValidResourceEntryName("_FøøBar")).isTrue() + + Truth.assertThat(isValidResourceEntryName("12FøøBar")).isFalse() + Truth.assertThat(isValidResourceEntryName("Føø\$Bar")).isFalse() + Truth.assertThat(isValidResourceEntryName("Føø/Bar")).isFalse() + Truth.assertThat(isValidResourceEntryName("Føø:Bar")).isFalse() + Truth.assertThat(isValidResourceEntryName("Føø;Bar")).isFalse() + Truth.assertThat(isValidResourceEntryName("0_resource_name_obfuscated")).isFalse() + } +} \ No newline at end of file diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/XmlProcessorTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/XmlProcessorTest.kt new file mode 100644 index 000000000..bc57d695f --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/XmlProcessorTest.kt @@ -0,0 +1,697 @@ +package com.android.aaptcompiler + +import com.android.aaptcompiler.testutils.parseNameOrFail +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +/** + * Tests for the XmlProcessor, which parses XML files found in /res/xml* directories. + * + * TODO(b/132800341): add a connected test to verify contents at runtime. + * If you need to manually verify the correct behaviour, place the rest file under the res/xml + * folder. To read contents you can use the XmlPullParser at runtime in an android project, for + * example to verify the strings are parsed correctly you can query: + * + * val text = view.findViewById(R.id.text) + * val xml = view.resources.getXml(R.xml.test) + * var eventType = -1 + * + * val builder = StringBuilder() + * while (eventType != XmlResourceParser.END_DOCUMENT) { + * if (xml.eventType == XmlResourceParser.TEXT) { + * builder.append("${xml.name} = \"${xml.text}\"\n") + * } + * + * eventType = xml.next() + * } + * text.text = "Found:\n${builder.toString()}" + */ +class XmlProcessorTest { + + lateinit var processor: XmlProcessor + + @Before + fun beforeTest() { + processor = XmlProcessor(Source(""), null) + } + + private fun processTest(input: String, configDescription: String = ""): ResourceFile? { + val file = ResourceFile( + parseNameOrFail("layout/test"), + parse(configDescription), + Source(""), + ResourceFile.Type.ProtoXml) + try { + processor.process(file, input.byteInputStream()) + } catch (e: Exception) { + return null + } + return file + } + + private fun getAttrName(inputFile: ResourceFile, attrNum: Int): ResourceName = + inputFile.name.copy(entry = "${inputFile.name.pck}${'$'}${inputFile.name.entry}__$attrNum") + + @Test + fun testEmpty() { + assertThat(processTest("")).isNotNull() + } + + @Test + fun testCollectsIds() { + val input = """ + + + + """.trimIndent() + + assertThat(processTest(input)).isNotNull() + + val collectedIds = processor.primaryFile.exportedSymbols + assertThat(collectedIds).hasSize(3) + + assertThat(collectedIds[0]).isEqualTo(SourcedResourceName(parseNameOrFail("id/bar"), 3)) + assertThat(collectedIds[1]).isEqualTo(SourcedResourceName(parseNameOrFail("id/car"), 6)) + assertThat(collectedIds[2]).isEqualTo(SourcedResourceName(parseNameOrFail("id/foo"), 3)) + } + + @Test + fun testNoCollectNonIds() { + assertThat(processTest("""""")).isNotNull() + + assertThat(processor.primaryFile.exportedSymbols).isEmpty() + + val input = """ + + """.trimIndent() + assertThat(processTest(input)).isNotNull() + + assertThat(processor.primaryFile.exportedSymbols).hasSize(1) + assertThat(processor.primaryFile.exportedSymbols[0]) + .isEqualTo(SourcedResourceName(parseNameOrFail("id/foo"), 3)) + } + + @Test + fun failOnInvalidIds() { + assertThat(processTest("""""")).isNull() + } + + @Test + fun checkNoSpaceAdded() { + // XMLEventReader parser divides this string into two for some reason. When we're processing + // it, we should make sure we join them without a new space, keeping the original string + // intact. + val stringDividedByReader = "1a(2d1r1u]1o2r3c4u1]1o2l1a(2l2u" + val input = """$stringDividedByReader""" + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(1) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("l") + assertThat(element.attributeList).hasSize(0) + assertThat(element.childList).hasSize(1) + + // No extra spaces should be added in the middle of the string (even if XMLEventReader divides + // it). + assertThat(element.getChild(0).text).isEqualTo(stringDividedByReader) + } + + @Test + fun checkNoSpacesRemoved() { + val contentWithSpaces = " hello world! We meet again " + val input = """$contentWithSpaces""" + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(1) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("l") + assertThat(element.attributeList).hasSize(0) + assertThat(element.childList).hasSize(1) + + // Extra spaces should be kept. + assertThat(element.getChild(0).text).isEqualTo(contentWithSpaces) + } + + @Test + fun checkNoNewLineRemoved() { + val contentWithNewLines = """ + + Text that is + continued here. + """.trimIndent() + + val inputFile = processTest(contentWithNewLines) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(1) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("test") + assertThat(element.attributeList).hasSize(0) + assertThat(element.childList).hasSize(1) + + + // Indentation versus the parent node should be kept here. New lines should be respected. + assertThat(element.getChild(0).text).isEqualTo(""" + Text that is + continued here. +""") + } + + @Test + fun checkWhitespaceComboIsNotRemoved() { + val contentWithNewLines = """ + + hello world ! + + +We meet + + again + """ + val input = """$contentWithNewLines""" + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(1) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("l") + assertThat(element.attributeList).hasSize(0) + assertThat(element.childList).hasSize(1) + + // We should keep the new lines and extra whitespaces, just as AAPT2 does in res/xml* files. + assertThat(element.getChild(0).text).isEqualTo(contentWithNewLines) + } + + @Test + fun checkTextWithComment() { + val textWithComment = """ + Text that is + + broken by child.""".trimIndent() + + val input = """$textWithComment""" + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(1) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("l") + assertThat(element.attributeList).hasSize(0) + assertThat(element.childList).hasSize(2) + + // New lines are kept (including new lines), only the comment is removed. + assertThat(element.getChild(0).text).isEqualTo("Text that is\n") + assertThat(element.getChild(1).text).isEqualTo("\nbroken by child.") + } + + @Test + fun testNoInlineXml() { + val input = """ + + + + + + """.trimIndent() + + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(1) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("View") + assertThat(element.namespaceDeclarationList).hasSize(1) + assertThat(element.attributeList).hasSize(0) + assertThat(element.childList).hasSize(1) + + val namespace = element.namespaceDeclarationList[0] + assertThat(namespace.uri).isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(namespace.prefix).isEqualTo("android") + + val childNode = element.childList[0] + assertThat(childNode.hasElement()).isTrue() + + val child = childNode.element + assertThat(child.name).isEqualTo("View") + assertThat(child.namespaceDeclarationList).hasSize(0) + assertThat(child.attributeList).hasSize(1) + assertThat(child.childList).hasSize(1) + + val childAttr = child.attributeList[0] + assertThat(childAttr.namespaceUri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(childAttr.name).isEqualTo("text") + assertThat(childAttr.value).isEqualTo("hey") + + val grandchildNode = child.childList[0] + assertThat(grandchildNode.hasElement()).isTrue() + + val grandchild = grandchildNode.element + assertThat(grandchild.name).isEqualTo("View") + assertThat(grandchild.namespaceDeclarationList).hasSize(0) + assertThat(grandchild.attributeList).hasSize(1) + assertThat(grandchild.childList).hasSize(0) + + val grandchildAttr = grandchild.attributeList[0] + assertThat(grandchildAttr.namespaceUri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(grandchildAttr.name).isEqualTo("id") + assertThat(grandchildAttr.value).isEqualTo("hi") + } + + @Test + fun testExtractOneXmlResource() { + val input = """ + + + + + + + + """.trimIndent() + + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(2) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("View1") + assertThat(element.namespaceDeclarationList).hasSize(2) + assertThat(element.attributeList).hasSize(1) + assertThat(element.childList).hasSize(0) + + val namespace1 = element.namespaceDeclarationList[0] + assertThat(namespace1.uri).isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(namespace1.prefix).isEqualTo("android") + + val namespace2 = element.namespaceDeclarationList[1] + assertThat(namespace2.uri).isEqualTo("http://schemas.android.com/aapt") + assertThat(namespace2.prefix).isEqualTo("aapt") + + // the aapt:attr should be pulled out as an attribute. + // i.e. 'android:text="@layout/$test__0"' + val attr = element.attributeList[0] + assertThat(attr.namespaceUri).isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(attr.name).isEqualTo("text") + assertThat(attr.value).isEqualTo("@${getAttrName(inputFile!!, 0)}") + + val outlinedProto = processor.xmlResources[1].xmlProto + assertThat(outlinedProto).isNotNull() + assertThat(outlinedProto.hasElement()).isTrue() + + val outlinedElement = outlinedProto.element + assertThat(outlinedElement.name).isEqualTo("View2") + // The outlined element inherits all active namespace declarations. + assertThat(outlinedElement.namespaceDeclarationList).hasSize(2) + assertThat(outlinedElement.attributeList).hasSize(1) + assertThat(outlinedElement.childList).hasSize(1) + + val outlinedNamespace1 = outlinedElement.namespaceDeclarationList[0] + assertThat(outlinedNamespace1.uri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(outlinedNamespace1.prefix).isEqualTo("android") + + val outlinedNamespace2 = outlinedElement.namespaceDeclarationList[1] + assertThat(outlinedNamespace2.uri).isEqualTo("http://schemas.android.com/aapt") + assertThat(outlinedNamespace2.prefix).isEqualTo("aapt") + + val outlinedAttr = outlinedElement.attributeList[0] + assertThat(outlinedAttr.namespaceUri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(outlinedAttr.name).isEqualTo("text") + assertThat(outlinedAttr.value).isEqualTo("hey") + + val outlinedChildNode = outlinedElement.childList[0] + assertThat(outlinedChildNode.hasElement()).isTrue() + + val outlinedChild = outlinedChildNode.element + assertThat(outlinedChild.name).isEqualTo("View3") + assertThat(outlinedChild.namespaceDeclarationList).hasSize(0) + assertThat(outlinedChild.attributeList).hasSize(1) + assertThat(outlinedChild.childList).hasSize(0) + + val outlinedChildAttr = outlinedChild.attributeList[0] + assertThat(outlinedChildAttr.namespaceUri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(outlinedChildAttr.name).isEqualTo("id") + assertThat(outlinedChildAttr.value).isEqualTo("hi") + } + + @Test + fun testExtractTwoSiblingResources() { + val input = """ + + + + + + + + + + + + """.trimIndent() + + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(3) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("View1") + assertThat(element.namespaceDeclarationList).hasSize(2) + assertThat(element.attributeList).hasSize(2) + assertThat(element.childList).hasSize(0) + + val namespace1 = element.namespaceDeclarationList[0] + assertThat(namespace1.uri).isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(namespace1.prefix).isEqualTo("android") + + val namespace2 = element.namespaceDeclarationList[1] + assertThat(namespace2.uri).isEqualTo("http://schemas.android.com/aapt") + assertThat(namespace2.prefix).isEqualTo("aapt") + + // the aapt:attr should be pulled out as an attribute. + // i.e. 'android:text="@layout/$test__0"' + val attr1 = element.attributeList[0] + assertThat(attr1.namespaceUri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(attr1.name).isEqualTo("text") + assertThat(attr1.value).isEqualTo("@${getAttrName(inputFile!!, 0)}") + + // The second should be pulled out as well. + // i.e. 'android:drawable="@layout/$test__1"' + val attr2 = element.attributeList[1] + assertThat(attr2.namespaceUri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(attr2.name).isEqualTo("drawable") + assertThat(attr2.value).isEqualTo("@${getAttrName(inputFile, 1)}") + + val outlinedProto = processor.xmlResources[1].xmlProto + assertThat(outlinedProto).isNotNull() + assertThat(outlinedProto.hasElement()).isTrue() + + val outlinedElement = outlinedProto.element + assertThat(outlinedElement.name).isEqualTo("View2") + // The outlined element inherits all active namespace declarations. + assertThat(outlinedElement.namespaceDeclarationList).hasSize(2) + assertThat(outlinedElement.attributeList).hasSize(1) + assertThat(outlinedElement.childList).hasSize(1) + + val outlinedNamespace1 = outlinedElement.namespaceDeclarationList[0] + assertThat(outlinedNamespace1.uri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(outlinedNamespace1.prefix).isEqualTo("android") + + val outlinedNamespace2 = outlinedElement.namespaceDeclarationList[1] + assertThat(outlinedNamespace2.uri).isEqualTo("http://schemas.android.com/aapt") + assertThat(outlinedNamespace2.prefix).isEqualTo("aapt") + + val outlinedAttr = outlinedElement.attributeList[0] + assertThat(outlinedAttr.namespaceUri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(outlinedAttr.name).isEqualTo("text") + assertThat(outlinedAttr.value).isEqualTo("hey") + + val outlinedChildNode = outlinedElement.childList[0] + assertThat(outlinedChildNode.hasElement()).isTrue() + + val outlinedChild = outlinedChildNode.element + assertThat(outlinedChild.name).isEqualTo("View3") + assertThat(outlinedChild.namespaceDeclarationList).hasSize(0) + assertThat(outlinedChild.attributeList).hasSize(1) + assertThat(outlinedChild.childList).hasSize(0) + + val outlinedChildAttr = outlinedChild.attributeList[0] + assertThat(outlinedChildAttr.namespaceUri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(outlinedChildAttr.name).isEqualTo("id") + assertThat(outlinedChildAttr.value).isEqualTo("hi") + + val outlinedProto2 = processor.xmlResources[2].xmlProto + assertThat(outlinedProto2).isNotNull() + assertThat(outlinedProto2.hasElement()).isTrue() + + val outlined2Element = outlinedProto2.element + assertThat(outlined2Element.name).isEqualTo("vector") + assertThat(outlined2Element.namespaceDeclarationList).hasSize(2) + assertThat(outlined2Element.attributeList).hasSize(0) + assertThat(outlined2Element.childList).hasSize(0) + + val outlined2Namespace1 = outlinedElement.namespaceDeclarationList[0] + assertThat(outlined2Namespace1.uri) + .isEqualTo("http://schemas.android.com/apk/res/android") + assertThat(outlined2Namespace1.prefix).isEqualTo("android") + + val outlined2Namespace2 = outlinedElement.namespaceDeclarationList[1] + assertThat(outlined2Namespace2.uri).isEqualTo("http://schemas.android.com/aapt") + assertThat(outlined2Namespace2.prefix).isEqualTo("aapt") + } + + @Test + fun testExtractNestedXmlResource() { + val input = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """.trimIndent() + + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + // The primary file and the 8 outlined aapt:attr's + assertThat(processor.xmlResources).hasSize(9) + } + + @Test + fun testExtractIntoAppAttr() { + val input = """ + + + + + + """.trimIndent() + + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(2) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("parent") + assertThat(element.namespaceDeclarationList).hasSize(2) + assertThat(element.attributeList).hasSize(1) + assertThat(element.childList).hasSize(0) + + val namespace1 = element.namespaceDeclarationList[0] + assertThat(namespace1.uri).isEqualTo("http://schemas.android.com/apk/res-auto") + assertThat(namespace1.prefix).isEqualTo("app") + + val namespace2 = element.namespaceDeclarationList[1] + assertThat(namespace2.uri).isEqualTo("http://schemas.android.com/aapt") + assertThat(namespace2.prefix).isEqualTo("aapt") + + val attr1 = element.attributeList[0] + assertThat(attr1.namespaceUri) + .isEqualTo("http://schemas.android.com/apk/res-auto") + assertThat(attr1.name).isEqualTo("foo") + assertThat(attr1.value).isEqualTo("@${getAttrName(inputFile!!, 0)}") + } + + @Test + fun testExtractIntoNoNamespaceAttr() { + val input = """ + + + + + + """.trimIndent() + + val inputFile = processTest(input) + assertThat(inputFile).isNotNull() + + assertThat(processor.xmlResources).hasSize(2) + + val proto = processor.xmlResources[0].xmlProto + assertThat(proto).isNotNull() + assertThat(proto.hasElement()).isTrue() + + val element = proto.element + assertThat(element.name).isEqualTo("parent") + assertThat(element.namespaceDeclarationList).hasSize(1) + assertThat(element.attributeList).hasSize(1) + assertThat(element.childList).hasSize(0) + + val namespace = element.namespaceDeclarationList[0] + assertThat(namespace.uri).isEqualTo("http://schemas.android.com/aapt") + assertThat(namespace.prefix).isEqualTo("aapt") + + val attr1 = element.attributeList[0] + assertThat(attr1.namespaceUri).isEqualTo("") + assertThat(attr1.name).isEqualTo("foo") + assertThat(attr1.value).isEqualTo("@${getAttrName(inputFile!!, 0)}") + } + + @Test + fun failAttrRoot() { + val input = """ + + + + """.trimIndent() + + assertThat(processTest(input)).isNull() + } + + @Test + fun failConsecutiveNestedAttrDeclarations() { + val input =""" + + + + + + + + """.trimIndent() + + assertThat(processTest(input)).isNull() + } + + @Test + fun failToOverwriteExistingAttr() { + val input = """ + + + + + + """.trimIndent() + + assertThat(processTest(input)).isNull() + } + + @Test + fun failMultipleWritesToSameAttr() { + val input = """ + + + + + + + + + + + + + """.trimIndent() + + assertThat(processTest(input)).isNull() + } + + @Test + fun failEmptyAttr() { + val input = """ + + + + + """.trimIndent() + + assertThat(processTest(input)).isNull() + } +} diff --git a/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/XmlResourceBuilderTest.kt b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/XmlResourceBuilderTest.kt new file mode 100644 index 000000000..8ef57ad78 --- /dev/null +++ b/subprojects/aaptcompiler/src/test/java/com/android/aaptcompiler/XmlResourceBuilderTest.kt @@ -0,0 +1,386 @@ +package com.android.aaptcompiler + +import com.android.aaptcompiler.testutils.parseNameOrFail +import com.google.common.truth.Truth +import org.junit.Test + +class XmlResourceBuilderTest { + + private val fakeFile = ResourceFile( + parseNameOrFail("layout/foo"), + ConfigDescription(), + Source(""), + ResourceFile.Type.ProtoXml) + + @Test + fun testBasicXmlFlattening() { + /* + * + */ + val proto = XmlResourceBuilder(fakeFile) + .startElement("LinearLayout", "") + .addNamespaceDeclaration("http://schemas.android.com/apk/res/android", "android") + .addAttribute("layout_width", "http://schemas.android.com/apk/res/android", "match_parent") + .addAttribute("layout_height", "http://schemas.android.com/apk/res/android", "match_parent") + .endElement() + .build() + .xmlProto + + Truth.assertThat(proto.hasElement()).isTrue() + val root = proto.getElement() + Truth.assertThat(root.getNamespaceUri()).isEmpty() + Truth.assertThat(root.getName()).isEqualTo("LinearLayout") + val nsDeclarations = root.getNamespaceDeclarationList() + Truth.assertThat(nsDeclarations).hasSize(1) + val androidNs = nsDeclarations[0] + Truth.assertThat(androidNs.getUri()).isEqualTo("http://schemas.android.com/apk/res/android") + Truth.assertThat(androidNs.getPrefix()).isEqualTo("android") + val attributes = root.getAttributeList() + Truth.assertThat(attributes).hasSize(2) + val attribute1 = attributes[0] + Truth.assertThat(attribute1.getName()).isEqualTo("layout_width") + Truth.assertThat(attribute1.getNamespaceUri()) + .isEqualTo("http://schemas.android.com/apk/res/android") + Truth.assertThat(attribute1.getValue()).isEqualTo("match_parent") + val attribute2 = attributes[1] + Truth.assertThat(attribute2.getName()).isEqualTo("layout_height") + Truth.assertThat(attribute2.getNamespaceUri()) + .isEqualTo("http://schemas.android.com/apk/res/android") + Truth.assertThat(attribute2.getValue()).isEqualTo("match_parent") + Truth.assertThat(root.getChildList()).hasSize(0) + } + + @Test + fun testNestedXmlFlattening() { + /* + * + * + *