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