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