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