• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 isLeftEdgeToTheRight(other: Region): RegionSubject = apply {
334         val horizontallyPositionedToTheRight = other.bounds.left <= region.bounds.left
335         if (!horizontallyPositionedToTheRight) {
336             val errorMsgBuilder =
337                 ExceptionMessageBuilder()
338                     .forSubject(this)
339                     .forIncorrectRegion(
340                         "region. left edge of $region area should be to the right of $other"
341                     )
342                     .setExpected(other)
343                     .setActual(regionEntry.region)
344                     .addExtraDescription(
345                         "Horizontally positioned to the right",
346                         horizontallyPositionedToTheRight,
347                     )
348             throw IncorrectRegionException(errorMsgBuilder)
349         }
350     }
351 
352     /** {@inheritDoc} */
353     override fun regionsCenterPointInside(other: Rect): RegionSubject = apply {
354         if (!other.contains(region.bounds.centerX(), region.bounds.centerY())) {
355             val center = Point(region.bounds.centerX(), region.bounds.centerY())
356             val errorMsgBuilder =
357                 ExceptionMessageBuilder()
358                     .forSubject(this)
359                     .forIncorrectRegion("region. $region center point should be inside $other")
360                     .setExpected(other)
361                     .setActual(regionEntry.region)
362                     .addExtraDescription("Center point", center)
363             throw IncorrectRegionException(errorMsgBuilder)
364         }
365     }
366 
367     /** {@inheritDoc} */
368     override fun coversAtLeast(other: Region): RegionSubject = apply {
369         if (!region.coversAtLeast(other)) {
370             val errorMsgBuilder =
371                 ExceptionMessageBuilder()
372                     .forSubject(this)
373                     .forIncorrectRegion("region. $region should cover at least $other")
374                     .setExpected(other)
375                     .setActual(regionEntry.region)
376                     .addExtraDescription("Uncovered region", region.uncoveredRegion(other))
377             throw IncorrectRegionException(errorMsgBuilder)
378         }
379     }
380 
381     /** {@inheritDoc} */
382     override fun coversAtLeast(other: Rect): RegionSubject = coversAtLeast(Region(other))
383 
384     /** {@inheritDoc} */
385     override fun coversExactly(other: Region): RegionSubject = apply {
386         val intersection = Region(region)
387         val isNotEmpty = intersection.op(other, Region.Op.XOR)
388 
389         if (isNotEmpty) {
390             val errorMsgBuilder =
391                 ExceptionMessageBuilder()
392                     .forSubject(this)
393                     .forIncorrectRegion("region. $region should cover exactly $other")
394                     .setExpected(other)
395                     .setActual(regionEntry.region)
396                     .addExtraDescription("Difference", intersection)
397             throw IncorrectRegionException(errorMsgBuilder)
398         }
399     }
400 
401     /** {@inheritDoc} */
402     override fun coversExactly(other: Rect): RegionSubject = coversExactly(Region(other))
403 
404     /** {@inheritDoc} */
405     override fun overlaps(other: Region): RegionSubject = apply {
406         val intersection = Region(region)
407         val isEmpty = !intersection.op(other, Region.Op.INTERSECT)
408 
409         if (isEmpty) {
410             val errorMsgBuilder =
411                 ExceptionMessageBuilder()
412                     .forSubject(this)
413                     .forIncorrectRegion("region. $region should overlap with $other")
414                     .setExpected(other)
415                     .setActual(regionEntry.region)
416             throw IncorrectRegionException(errorMsgBuilder)
417         }
418     }
419 
420     /** {@inheritDoc} */
421     override fun overlaps(other: Rect): RegionSubject = overlaps(Region(other))
422 
423     /** {@inheritDoc} */
424     override fun notOverlaps(other: Region): RegionSubject = apply {
425         val intersection = Region(region)
426         val isEmpty = !intersection.op(other, Region.Op.INTERSECT)
427 
428         if (!isEmpty) {
429             val errorMsgBuilder =
430                 ExceptionMessageBuilder()
431                     .forSubject(this)
432                     .forIncorrectRegion("region. $region should not overlap with $other")
433                     .setExpected(other)
434                     .setActual(regionEntry.region)
435                     .addExtraDescription("Overlap region", intersection)
436             throw IncorrectRegionException(errorMsgBuilder)
437         }
438     }
439 
440     /** {@inheritDoc} */
441     override fun notOverlaps(other: Rect): RegionSubject = apply { notOverlaps(Region(other)) }
442 
443     /** {@inheritDoc} */
444     override fun isSameAspectRatio(other: Region, threshold: Double): RegionSubject = apply {
445         val thisBounds = this.region.bounds
446         val otherBounds = other.bounds
447         val aspectRatio = thisBounds.width().toFloat() / thisBounds.height()
448         val otherAspectRatio = otherBounds.width().toFloat() / otherBounds.height()
449         if (abs(aspectRatio - otherAspectRatio) > threshold) {
450             val errorMsgBuilder =
451                 ExceptionMessageBuilder()
452                     .forSubject(this)
453                     .forIncorrectRegion(
454                         "region. $region should have the same aspect ratio as $other"
455                     )
456                     .setExpected(other)
457                     .setActual(regionEntry.region)
458                     .addExtraDescription("Threshold", threshold)
459                     .addExtraDescription("Region aspect ratio", aspectRatio)
460                     .addExtraDescription("Other aspect ratio", otherAspectRatio)
461             throw IncorrectRegionException(errorMsgBuilder)
462         }
463     }
464 
465     /** {@inheritDoc} */
466     override fun hasSameBottomPosition(displayRect: Rect): RegionSubject = apply {
467         assertEquals("bottom", Region(displayRect)) { it.bottom }
468     }
469 
470     /** {@inheritDoc} */
471     override fun hasSameTopPosition(displayRect: Rect): RegionSubject = apply {
472         assertEquals("top", Region(displayRect)) { it.top }
473     }
474 
475     override fun hasSameLeftPosition(displayRect: Rect): RegionSubject = apply {
476         assertEquals("left", Region(displayRect)) { it.left }
477     }
478 
479     override fun hasSameRightPosition(displayRect: Rect): RegionSubject = apply {
480         assertEquals("right", Region(displayRect)) { it.right }
481     }
482 
483     fun isSameAspectRatio(other: RegionSubject, threshold: Double = 0.1): RegionSubject =
484         isSameAspectRatio(other.region, threshold)
485 
486     fun isSameAspectRatio(
487         numerator: Int,
488         denominator: Int,
489         threshold: Double = 0.1,
490     ): RegionSubject {
491         val region = Region()
492         region.set(Rect(0, 0, numerator, denominator))
493         return isSameAspectRatio(region, threshold)
494     }
495 
496     private fun <T : Comparable<T>> assertCompare(
497         name: String,
498         other: Region,
499         valueProvider: (Rect) -> T,
500         boundsCheck: (T, T) -> Boolean,
501     ) {
502         val thisValue = valueProvider(region.bounds)
503         val otherValue = valueProvider(other.bounds)
504         if (!boundsCheck(thisValue, otherValue)) {
505             val errorMsgBuilder =
506                 ExceptionMessageBuilder()
507                     .forSubject(this)
508                     .forIncorrectRegion(name)
509                     .setExpected(otherValue.toString())
510                     .setActual(thisValue.toString())
511                     .addExtraDescription("Actual region", region)
512                     .addExtraDescription("Expected region", other)
513             throw IncorrectRegionException(errorMsgBuilder)
514         }
515     }
516 
517     private fun <T : Comparable<T>> assertEquals(
518         name: String,
519         other: Region,
520         valueProvider: (Rect) -> T,
521     ) = assertCompare(name, other, valueProvider) { thisV, otherV -> thisV == otherV }
522 
523     private fun assertLeftRightAndAreaEquals(other: Region) {
524         assertEquals("left", other) { it.left }
525         assertEquals("right", other) { it.right }
526         assertEquals("area", other) { it.area }
527     }
528 
529     private fun assertTopBottomAndAreaEquals(other: Region) {
530         assertEquals("top", other) { it.top }
531         assertEquals("bottom", other) { it.bottom }
532         assertEquals("area", other) { it.area }
533     }
534 
535     companion object {
536         private fun mergeRegions(regions: Collection<Region>): Region {
537             val result = Region()
538             regions.forEach { region -> result.op(region, Region.Op.UNION) }
539             return result
540         }
541     }
542 }
543