1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.content.res 18 19 20 import android.platform.test.annotations.Presubmit 21 import androidx.core.util.forEach 22 import androidx.test.ext.junit.runners.AndroidJUnit4 23 import androidx.test.filters.LargeTest 24 import androidx.test.filters.SmallTest 25 import com.google.common.truth.Truth.assertThat 26 import com.google.common.truth.Truth.assertWithMessage 27 import kotlin.math.ceil 28 import kotlin.math.floor 29 import org.junit.Test 30 import org.junit.runner.RunWith 31 import kotlin.random.Random.Default.nextFloat 32 33 @Presubmit 34 @RunWith(AndroidJUnit4::class) 35 class FontScaleConverterFactoryTest { 36 37 @Test 38 fun scale200IsTwiceAtSmallSizes() { 39 val table = FontScaleConverterFactory.forScale(2F)!! 40 assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(2f) 41 assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(16f) 42 assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(20f) 43 assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f) 44 assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f) 45 } 46 47 @LargeTest 48 @Test 49 fun missingLookupTablePastEnd_returnsLinear() { 50 val table = FontScaleConverterFactory.forScale(3F)!! 51 generateSequenceOfFractions(-10000f..10000f, step = 0.01f) 52 .map { 53 assertThat(table.convertSpToDp(it)).isWithin(CONVERSION_TOLERANCE).of(it * 3f) 54 } 55 assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(3f) 56 assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(24f) 57 assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(30f) 58 assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(15f) 59 assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f) 60 assertThat(table.convertSpToDp(50F)).isWithin(CONVERSION_TOLERANCE).of(150f) 61 assertThat(table.convertSpToDp(100F)).isWithin(CONVERSION_TOLERANCE).of(300f) 62 } 63 64 @SmallTest 65 fun missingLookupTable110_returnsInterpolated() { 66 val table = FontScaleConverterFactory.forScale(1.1F)!! 67 68 assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1.1f) 69 assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f * 1.1f) 70 assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(11f) 71 assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f * 1.1f) 72 assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f) 73 assertThat(table.convertSpToDp(50F)).isLessThan(50f * 1.1f) 74 assertThat(table.convertSpToDp(100F)).isLessThan(100f * 1.1f) 75 } 76 77 @Test 78 fun missingLookupTable199_returnsInterpolated() { 79 val table = FontScaleConverterFactory.forScale(1.9999F)!! 80 assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(2f) 81 assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(16f) 82 assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(20f) 83 assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f) 84 assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f) 85 } 86 87 @Test 88 fun missingLookupTable160_returnsInterpolated() { 89 val table = FontScaleConverterFactory.forScale(1.6F)!! 90 assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1f * 1.6F) 91 assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f * 1.6F) 92 assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(10f * 1.6F) 93 assertThat(table.convertSpToDp(20F)).isLessThan(20f * 1.6F) 94 assertThat(table.convertSpToDp(100F)).isLessThan(100f * 1.6F) 95 assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f * 1.6F) 96 assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f) 97 } 98 99 @SmallTest 100 fun missingLookupTableNegativeReturnsNull() { 101 assertThat(FontScaleConverterFactory.forScale(-1F)).isNull() 102 } 103 104 @SmallTest 105 fun unnecessaryFontScalesReturnsNull() { 106 assertThat(FontScaleConverterFactory.forScale(0F)).isNull() 107 assertThat(FontScaleConverterFactory.forScale(1F)).isNull() 108 assertThat(FontScaleConverterFactory.forScale(0.85F)).isNull() 109 } 110 111 @SmallTest 112 fun tablesMatchAndAreMonotonicallyIncreasing() { 113 FontScaleConverterFactory.LOOKUP_TABLES.forEach { _, lookupTable -> 114 assertThat(lookupTable.mToDpValues).hasLength(lookupTable.mFromSpValues.size) 115 assertThat(lookupTable.mToDpValues).isNotEmpty() 116 117 assertThat(lookupTable.mFromSpValues.asList()).isInStrictOrder() 118 assertThat(lookupTable.mToDpValues.asList()).isInStrictOrder() 119 120 assertThat(lookupTable.mFromSpValues.asList()).containsNoDuplicates() 121 assertThat(lookupTable.mToDpValues.asList()).containsNoDuplicates() 122 } 123 } 124 125 @SmallTest 126 fun testIsNonLinearFontScalingActive() { 127 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1f)).isFalse() 128 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0f)).isFalse() 129 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(-1f)).isFalse() 130 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0.85f)).isFalse() 131 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.02f)).isFalse() 132 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isFalse() 133 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.15f)).isTrue() 134 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.1499999f)) 135 .isTrue() 136 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.5f)).isTrue() 137 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(2f)).isTrue() 138 assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(3f)).isTrue() 139 } 140 141 @LargeTest 142 @Test 143 fun allFeasibleScalesAndConversionsDoNotCrash() { 144 generateSequenceOfFractions(-10f..10f, step = 0.1f) 145 .fuzzFractions() 146 .mapNotNull{ FontScaleConverterFactory.forScale(it) } 147 .flatMap{ table -> 148 generateSequenceOfFractions(-2000f..2000f, step = 0.1f) 149 .fuzzFractions() 150 .map{ Pair(table, it) } 151 } 152 .forEach { (table, sp) -> 153 try { 154 // Truth is slow because it creates a bunch of 155 // objects. Don't use it unless we need to. 156 if (!table.convertSpToDp(sp).isFinite()) { 157 assertWithMessage("convertSpToDp(%s) on table: %s", sp, table) 158 .that(table.convertSpToDp(sp)) 159 .isFinite() 160 } 161 } catch (e: Exception) { 162 throw AssertionError("Exception during convertSpToDp($sp) on table: $table", e) 163 } 164 } 165 } 166 167 @Test 168 fun testGenerateSequenceOfFractions() { 169 val fractions = generateSequenceOfFractions(-1000f..1000f, step = 0.1f) 170 .toList() 171 fractions.forEach { 172 assertThat(it).isAtLeast(-1000f) 173 assertThat(it).isAtMost(1000f) 174 } 175 176 assertThat(fractions).isInStrictOrder() 177 assertThat(fractions).hasSize(1000 * 2 * 10 + 1) // Don't forget the 0 in the middle! 178 179 assertThat(fractions).contains(100f) 180 assertThat(fractions).contains(500.1f) 181 assertThat(fractions).contains(500.2f) 182 assertThat(fractions).contains(0.2f) 183 assertThat(fractions).contains(0f) 184 assertThat(fractions).contains(-10f) 185 assertThat(fractions).contains(-10f) 186 assertThat(fractions).contains(-10.3f) 187 188 assertThat(fractions).doesNotContain(-10.31f) 189 assertThat(fractions).doesNotContain(0.35f) 190 assertThat(fractions).doesNotContain(0.31f) 191 assertThat(fractions).doesNotContain(-.35f) 192 } 193 194 @Test 195 fun testFuzzFractions() { 196 val numFuzzedFractions = 6 197 val fractions = generateSequenceOfFractions(-1000f..1000f, step = 0.1f) 198 .fuzzFractions() 199 .toList() 200 fractions.forEach { 201 assertThat(it).isAtLeast(-1000f) 202 assertThat(it).isLessThan(1001f) 203 } 204 205 val numGeneratedFractions = 1000 * 2 * 10 + 1 // Don't forget the 0 in the middle! 206 assertThat(fractions).hasSize(numGeneratedFractions * numFuzzedFractions) 207 208 assertThat(fractions).contains(100f) 209 assertThat(fractions).contains(500.1f) 210 assertThat(fractions).contains(500.2f) 211 assertThat(fractions).contains(0.2f) 212 assertThat(fractions).contains(0f) 213 assertThat(fractions).contains(-10f) 214 assertThat(fractions).contains(-10f) 215 assertThat(fractions).contains(-10.3f) 216 } 217 218 companion object { 219 private const val CONVERSION_TOLERANCE = 0.05f 220 } 221 } 222 223 fun generateSequenceOfFractions( 224 range: ClosedFloatingPointRange<Float>, 225 step: Float 226 ): Sequence<Float> { 227 val multiplier = 1f / step 228 val start = floor(range.start * multiplier).toInt() 229 val endInclusive = ceil(range.endInclusive * multiplier).toInt() 230 return generateSequence(start) { it + 1 } 231 .takeWhile { it <= endInclusive } 232 .map{ it.toFloat() / multiplier } 233 } 234 235 private fun Sequence<Float>.fuzzFractions(): Sequence<Float> { 236 return flatMap { i -> 237 listOf(i, i + 0.01f, i + 0.054f, i + 0.099f, i + nextFloat(), i + nextFloat()) 238 } 239 } 240