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