1 /*
<lambda>null2  * Copyright (C) 2023 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.tools.flicker.subject.region
18 
19 import android.graphics.Point
20 import android.graphics.Rect
21 import android.graphics.RectF
22 import android.graphics.Region
23 import android.tools.Timestamp
24 import android.tools.datatypes.coversAtLeast
25 import android.tools.datatypes.coversAtMost
26 import android.tools.datatypes.outOfBoundsRegion
27 import android.tools.datatypes.uncoveredRegion
28 import android.tools.flicker.subject.FlickerSubject
29 import android.tools.flicker.subject.exceptions.ExceptionMessageBuilder
30 import android.tools.flicker.subject.exceptions.IncorrectRegionException
31 import android.tools.io.Reader
32 import android.tools.traces.region.RegionEntry
33 import androidx.core.graphics.toRect
34 import kotlin.math.abs
35 
36 /**
37  * Subject for [Region] objects, used to make assertions over behaviors that occur on a rectangle.
38  */
39 class RegionSubject(
40     val regionEntry: RegionEntry,
41     override val timestamp: Timestamp,
42     override val reader: Reader? = null,
43 ) : FlickerSubject(), IRegionSubject {
44 
45     /** Custom constructor for existing android regions */
46     constructor(
47         region: Region?,
48         timestamp: Timestamp,
49         reader: Reader? = null
50     ) : this(RegionEntry(region ?: Region(), timestamp), timestamp, reader)
51 
52     /** Custom constructor for existing rects */
53     constructor(
54         rect: Rect?,
55         timestamp: Timestamp,
56         reader: Reader? = null
57     ) : this(Region(rect ?: Rect()), timestamp, reader)
58 
59     /** Custom constructor for existing rects */
60     constructor(
61         rect: RectF?,
62         timestamp: Timestamp,
63         reader: Reader? = null
64     ) : this(rect?.toRect(), timestamp, reader)
65 
66     /** Custom constructor for existing regions */
67     constructor(
68         regions: Collection<Region>,
69         timestamp: Timestamp,
70         reader: Reader? = null
71     ) : this(mergeRegions(regions), timestamp, reader)
72 
73     val region = regionEntry.region
74 
75     private val Rect.area
76         get() = this.width() * this.height()
77 
78     /**
79      * Asserts that the current [Region] doesn't contain layers
80      *
81      * @throws AssertionError
82      */
83     fun isEmpty(): RegionSubject = apply {
84         if (!regionEntry.region.isEmpty) {
85             val errorMsgBuilder =
86                 ExceptionMessageBuilder()
87                     .forSubject(this)
88                     .forIncorrectRegion("region")
89                     .setExpected(Region())
90                     .setActual(regionEntry.region)
91             throw IncorrectRegionException(errorMsgBuilder)
92         }
93     }
94 
95     /**
96      * Asserts that the current [Region] doesn't contain layers
97      *
98      * @throws AssertionError
99      */
100     fun isNotEmpty(): RegionSubject = apply {
101         if (regionEntry.region.isEmpty) {
102             val errorMsgBuilder =
103                 ExceptionMessageBuilder()
104                     .forSubject(this)
105                     .forIncorrectRegion("region")
106                     .setExpected("Not empty")
107                     .setActual(regionEntry.region)
108             throw IncorrectRegionException(errorMsgBuilder)
109         }
110     }
111 
112     /** Subtracts [other] from this subject [region] */
113     fun minus(other: Region): RegionSubject {
114         val remainingRegion = Region(this.region)
115         remainingRegion.op(other, Region.Op.XOR)
116         return RegionSubject(remainingRegion, timestamp, reader)
117     }
118 
119     /** Adds [other] to this subject [region] */
120     fun plus(other: Region): RegionSubject {
121         val remainingRegion = Region(this.region)
122         remainingRegion.op(other, Region.Op.UNION)
123         return RegionSubject(remainingRegion, timestamp, reader)
124     }
125 
126     /** See [isHigherOrEqual] */
127     fun isHigherOrEqual(subject: RegionSubject): RegionSubject = isHigherOrEqual(subject.region)
128 
129     /** {@inheritDoc} */
130     override fun isHigherOrEqual(other: Rect): RegionSubject = isHigherOrEqual(Region(other))
131 
132     /** {@inheritDoc} */
133     override fun isHigherOrEqual(other: Region): RegionSubject = apply {
134         assertLeftRightAndAreaEquals(other)
135         assertCompare(
136             name = "top position. Expected to be higher or equal",
137             other,
138             { it.top },
139             { thisV, otherV -> thisV <= otherV }
140         )
141         assertCompare(
142             name = "bottom position. Expected to be higher or equal",
143             other,
144             { it.bottom },
145             { thisV, otherV -> thisV <= otherV }
146         )
147     }
148 
149     /** See [isLowerOrEqual] */
150     fun isLowerOrEqual(subject: RegionSubject): RegionSubject = isLowerOrEqual(subject.region)
151 
152     /** {@inheritDoc} */
153     override fun isLowerOrEqual(other: Rect): RegionSubject = isLowerOrEqual(Region(other))
154 
155     /** {@inheritDoc} */
156     override fun isLowerOrEqual(other: Region): RegionSubject = apply {
157         assertLeftRightAndAreaEquals(other)
158         assertCompare(
159             name = "top position. Expected to be lower or equal",
160             other,
161             { it.top },
162             { thisV, otherV -> thisV >= otherV }
163         )
164         assertCompare(
165             name = "bottom position. Expected to be lower or equal",
166             other,
167             { it.bottom },
168             { thisV, otherV -> thisV >= otherV }
169         )
170     }
171 
172     /** {@inheritDoc} */
173     override fun isToTheRight(other: Region): RegionSubject = apply {
174         assertTopBottomAndAreaEquals(other)
175         assertCompare(
176             name = "left position. Expected to be lower or equal",
177             other,
178             { it.left },
179             { thisV, otherV -> thisV >= otherV }
180         )
181         assertCompare(
182             name = "right position. Expected to be lower or equal",
183             other,
184             { it.right },
185             { thisV, otherV -> thisV >= otherV }
186         )
187     }
188 
189     /** See [isHigher] */
190     fun isHigher(subject: RegionSubject): RegionSubject = isHigher(subject.region)
191 
192     /** {@inheritDoc} */
193     override fun isHigher(other: Rect): RegionSubject = isHigher(Region(other))
194 
195     /** {@inheritDoc} */
196     override fun isHigher(other: Region): RegionSubject = apply {
197         assertLeftRightAndAreaEquals(other)
198         assertCompare(
199             name = "top position. Expected to be higher",
200             other,
201             { it.top },
202             { thisV, otherV -> thisV < otherV }
203         )
204         assertCompare(
205             name = "bottom position. Expected to be higher",
206             other,
207             { it.bottom },
208             { thisV, otherV -> thisV < otherV }
209         )
210     }
211 
212     /** See [isLower] */
213     fun isLower(subject: RegionSubject): RegionSubject = isLower(subject.region)
214 
215     /** {@inheritDoc} */
216     override fun isLower(other: Rect): RegionSubject = isLower(Region(other))
217 
218     /**
219      * Asserts that the top and bottom coordinates of [other] are greater than those of [region].
220      *
221      * Also checks that the left and right positions, as well as area, don't change
222      *
223      * @throws IncorrectRegionException
224      */
225     override fun isLower(other: Region): RegionSubject = apply {
226         assertLeftRightAndAreaEquals(other)
227         assertCompare(
228             name = "top position. Expected to be lower",
229             other,
230             { it.top },
231             { thisV, otherV -> thisV > otherV }
232         )
233         assertCompare(
234             name = "bottom position. Expected to be lower",
235             other,
236             { it.bottom },
237             { thisV, otherV -> thisV > otherV }
238         )
239     }
240 
241     /** {@inheritDoc} */
242     override fun coversAtMost(other: Region): RegionSubject = apply {
243         if (!region.coversAtMost(other)) {
244             val errorMsgBuilder =
245                 ExceptionMessageBuilder()
246                     .forSubject(this)
247                     .forIncorrectRegion("region. $region should cover at most $other")
248                     .setExpected(other)
249                     .setActual(regionEntry.region)
250                     .addExtraDescription("Out-of-bounds region", region.outOfBoundsRegion(other))
251             throw IncorrectRegionException(errorMsgBuilder)
252         }
253     }
254 
255     /** {@inheritDoc} */
256     override fun coversAtMost(other: Rect): RegionSubject = coversAtMost(Region(other))
257 
258     /** {@inheritDoc} */
259     override fun notBiggerThan(other: Region): RegionSubject = apply {
260         val testArea = other.bounds.area
261         val area = region.bounds.area
262 
263         if (area > testArea) {
264             val errorMsgBuilder =
265                 ExceptionMessageBuilder()
266                     .forSubject(this)
267                     .forIncorrectRegion("region. $region area should not be bigger than $testArea")
268                     .setExpected(testArea)
269                     .setActual(area)
270                     .addExtraDescription("Expected region", other)
271                     .addExtraDescription("Actual region", regionEntry.region)
272             throw IncorrectRegionException(errorMsgBuilder)
273         }
274     }
275 
276     /** {@inheritDoc} */
277     override fun isToTheRightBottom(other: Region, threshold: Int): RegionSubject = apply {
278         val horizontallyPositionedToTheRight = other.bounds.left - threshold <= region.bounds.left
279         val verticallyPositionedToTheBottom = other.bounds.top - threshold <= region.bounds.top
280 
281         if (!horizontallyPositionedToTheRight || !verticallyPositionedToTheBottom) {
282             val errorMsgBuilder =
283                 ExceptionMessageBuilder()
284                     .forSubject(this)
285                     .forIncorrectRegion(
286                         "region. $region area should be to the right bottom of $other"
287                     )
288                     .setExpected(other)
289                     .setActual(regionEntry.region)
290                     .addExtraDescription("Threshold", threshold)
291                     .addExtraDescription(
292                         "Horizontally positioned to the right",
293                         horizontallyPositionedToTheRight
294                     )
295                     .addExtraDescription(
296                         "Vertically positioned to the bottom",
297                         verticallyPositionedToTheBottom
298                     )
299             throw IncorrectRegionException(errorMsgBuilder)
300         }
301     }
302 
303     /** {@inheritDoc} */
304     override fun regionsCenterPointInside(other: Rect): RegionSubject = apply {
305         if (!other.contains(region.bounds.centerX(), region.bounds.centerY())) {
306             val center = Point(region.bounds.centerX(), region.bounds.centerY())
307             val errorMsgBuilder =
308                 ExceptionMessageBuilder()
309                     .forSubject(this)
310                     .forIncorrectRegion("region. $region center point should be inside $other")
311                     .setExpected(other)
312                     .setActual(regionEntry.region)
313                     .addExtraDescription("Center point", center)
314             throw IncorrectRegionException(errorMsgBuilder)
315         }
316     }
317 
318     /** {@inheritDoc} */
319     override fun coversAtLeast(other: Region): RegionSubject = apply {
320         if (!region.coversAtLeast(other)) {
321             val errorMsgBuilder =
322                 ExceptionMessageBuilder()
323                     .forSubject(this)
324                     .forIncorrectRegion("region. $region should cover at least $other")
325                     .setExpected(other)
326                     .setActual(regionEntry.region)
327                     .addExtraDescription("Uncovered region", region.uncoveredRegion(other))
328             throw IncorrectRegionException(errorMsgBuilder)
329         }
330     }
331 
332     /** {@inheritDoc} */
333     override fun coversAtLeast(other: Rect): RegionSubject = coversAtLeast(Region(other))
334 
335     /** {@inheritDoc} */
336     override fun coversExactly(other: Region): RegionSubject = apply {
337         val intersection = Region(region)
338         val isNotEmpty = intersection.op(other, Region.Op.XOR)
339 
340         if (isNotEmpty) {
341             val errorMsgBuilder =
342                 ExceptionMessageBuilder()
343                     .forSubject(this)
344                     .forIncorrectRegion("region. $region should cover exactly $other")
345                     .setExpected(other)
346                     .setActual(regionEntry.region)
347                     .addExtraDescription("Difference", intersection)
348             throw IncorrectRegionException(errorMsgBuilder)
349         }
350     }
351 
352     /** {@inheritDoc} */
353     override fun coversExactly(other: Rect): RegionSubject = coversExactly(Region(other))
354 
355     /** {@inheritDoc} */
356     override fun overlaps(other: Region): RegionSubject = apply {
357         val intersection = Region(region)
358         val isEmpty = !intersection.op(other, Region.Op.INTERSECT)
359 
360         if (isEmpty) {
361             val errorMsgBuilder =
362                 ExceptionMessageBuilder()
363                     .forSubject(this)
364                     .forIncorrectRegion("region. $region should overlap with $other")
365                     .setExpected(other)
366                     .setActual(regionEntry.region)
367             throw IncorrectRegionException(errorMsgBuilder)
368         }
369     }
370 
371     /** {@inheritDoc} */
372     override fun overlaps(other: Rect): RegionSubject = overlaps(Region(other))
373 
374     /** {@inheritDoc} */
375     override fun notOverlaps(other: Region): RegionSubject = apply {
376         val intersection = Region(region)
377         val isEmpty = !intersection.op(other, Region.Op.INTERSECT)
378 
379         if (!isEmpty) {
380             val errorMsgBuilder =
381                 ExceptionMessageBuilder()
382                     .forSubject(this)
383                     .forIncorrectRegion("region. $region should not overlap with $other")
384                     .setExpected(other)
385                     .setActual(regionEntry.region)
386                     .addExtraDescription("Overlap region", intersection)
387             throw IncorrectRegionException(errorMsgBuilder)
388         }
389     }
390 
391     /** {@inheritDoc} */
392     override fun notOverlaps(other: Rect): RegionSubject = apply { notOverlaps(Region(other)) }
393 
394     /** {@inheritDoc} */
395     override fun isSameAspectRatio(other: Region, threshold: Double): RegionSubject = apply {
396         val thisBounds = this.region.bounds
397         val otherBounds = other.bounds
398         val aspectRatio = thisBounds.width().toFloat() / thisBounds.height()
399         val otherAspectRatio = otherBounds.width().toFloat() / otherBounds.height()
400         if (abs(aspectRatio - otherAspectRatio) > threshold) {
401             val errorMsgBuilder =
402                 ExceptionMessageBuilder()
403                     .forSubject(this)
404                     .forIncorrectRegion(
405                         "region. $region should have the same aspect ratio as $other"
406                     )
407                     .setExpected(other)
408                     .setActual(regionEntry.region)
409                     .addExtraDescription("Threshold", threshold)
410                     .addExtraDescription("Region aspect ratio", aspectRatio)
411                     .addExtraDescription("Other aspect ratio", otherAspectRatio)
412             throw IncorrectRegionException(errorMsgBuilder)
413         }
414     }
415 
416     /** {@inheritDoc} */
417     override fun hasSameBottomPosition(displayRect: Rect): RegionSubject = apply {
418         assertEquals("bottom", Region(displayRect)) { it.bottom }
419     }
420 
421     /** {@inheritDoc} */
422     override fun hasSameTopPosition(displayRect: Rect): RegionSubject = apply {
423         assertEquals("top", Region(displayRect)) { it.top }
424     }
425 
426     override fun hasSameLeftPosition(displayRect: Rect): RegionSubject = apply {
427         assertEquals("left", Region(displayRect)) { it.left }
428     }
429 
430     override fun hasSameRightPosition(displayRect: Rect): RegionSubject = apply {
431         assertEquals("right", Region(displayRect)) { it.right }
432     }
433 
434     fun isSameAspectRatio(other: RegionSubject, threshold: Double = 0.1): RegionSubject =
435         isSameAspectRatio(other.region, threshold)
436 
437     fun isSameAspectRatio(
438         numerator: Int,
439         denominator: Int,
440         threshold: Double = 0.1
441     ): RegionSubject {
442         val region = Region()
443         region.set(Rect(0, 0, numerator, denominator))
444         return isSameAspectRatio(region, threshold)
445     }
446 
447     private fun <T : Comparable<T>> assertCompare(
448         name: String,
449         other: Region,
450         valueProvider: (Rect) -> T,
451         boundsCheck: (T, T) -> Boolean
452     ) {
453         val thisValue = valueProvider(region.bounds)
454         val otherValue = valueProvider(other.bounds)
455         if (!boundsCheck(thisValue, otherValue)) {
456             val errorMsgBuilder =
457                 ExceptionMessageBuilder()
458                     .forSubject(this)
459                     .forIncorrectRegion(name)
460                     .setExpected(otherValue.toString())
461                     .setActual(thisValue.toString())
462                     .addExtraDescription("Actual region", region)
463                     .addExtraDescription("Expected region", other)
464             throw IncorrectRegionException(errorMsgBuilder)
465         }
466     }
467 
468     private fun <T : Comparable<T>> assertEquals(
469         name: String,
470         other: Region,
471         valueProvider: (Rect) -> T
472     ) = assertCompare(name, other, valueProvider) { thisV, otherV -> thisV == otherV }
473 
474     private fun assertLeftRightAndAreaEquals(other: Region) {
475         assertEquals("left", other) { it.left }
476         assertEquals("right", other) { it.right }
477         assertEquals("area", other) { it.area }
478     }
479 
480     private fun assertTopBottomAndAreaEquals(other: Region) {
481         assertEquals("top", other) { it.top }
482         assertEquals("bottom", other) { it.bottom }
483         assertEquals("area", other) { it.area }
484     }
485 
486     companion object {
487         private fun mergeRegions(regions: Collection<Region>): Region {
488             val result = Region()
489             regions.forEach { region -> result.op(region, Region.Op.UNION) }
490             return result
491         }
492     }
493 }
494