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