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