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