[go: nahoru, domu]

blob: 0b6238b34ac150bb65f00f3059832b69aa11ccc0 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.text.matchers
import android.text.Spanned
import com.google.common.truth.Fact.simpleFact
import com.google.common.truth.FailureMetadata
import com.google.common.truth.IterableSubject
import com.google.common.truth.Subject
import com.google.common.truth.Subject.Factory
import com.google.common.truth.Truth.assertAbout
import kotlin.reflect.KClass
/**
* Truth extension for CharSequence used for Span related checks.
*/
internal class CharSequenceSubject private constructor(
failureMetadata: FailureMetadata?,
private val subject: CharSequence?
) : Subject(failureMetadata, subject) {
companion object {
internal val SUBJECT_FACTORY: Factory<CharSequenceSubject?, CharSequence?> =
Factory { failureMetadata, subject -> CharSequenceSubject(failureMetadata, subject) }
}
fun <T : Any> spans(spanClazz: KClass<out T>): SpanIterableSubject<T> {
check("isNotNull()").that(subject).isNotNull()
check("instanceOf()").that(subject).isInstanceOf(Spanned::class.java)
val spanned = subject as Spanned
val spans = spanned.getSpans(0, spanned.length, spanClazz.java).map {
SpanInfo(it, spanned.getSpanStart(it), spanned.getSpanEnd(it), spanned.getSpanFlags(it))
}
return assertAbout(SpanIterableSubject.factory(spanClazz)).that(spans)!!
}
/**
* Checks if the given text contains a span matching the given class and position.
*
* When [predicate] function is provided, for the span that is found, predicate will be called
* to further validate the span object.
*
* @param spanClazz the class of the expected span
* @param start start position of the expected span
* @param end end position of the expected span
* @param predicate function to further assert the span object
*/
fun <T : Any> hasSpan(
spanClazz: KClass<out T>,
start: Int,
end: Int,
predicate: ((T) -> Boolean)? = null
) {
spans(spanClazz).has(start, end, predicate)
}
/**
* Similar to [hasSpan], and the returned matcher will also check that the span is not covered
* by other spans.
*
* @param spanClazz the class of the expected span
* @param start start position of the expected span
* @param end end position of the expected span
* @param predicate function to further assert the span object
*/
fun <T : Any> hasSpanOnTop(
spanClazz: KClass<out T>,
start: Int,
end: Int,
predicate: ((T) -> Boolean)? = null
) {
spans(spanClazz).hasOnTop(start, end, predicate)
}
}
/**
* Truth extension for a list of Spans.
*/
internal class SpanIterableSubject<T : Any> private constructor(
failureMetadata: FailureMetadata?,
private val subjects: List<SpanInfo<out T>>?,
private val spanClazz: KClass<out T>
) : IterableSubject(failureMetadata, subjects) {
companion object {
fun <T : Any> factory(spanClazz: KClass<out T>): Factory<SpanIterableSubject<T>?,
List<SpanInfo<T>>?> {
return Factory { failureMetadata, subject ->
SpanIterableSubject(
failureMetadata,
subject,
spanClazz
)
}
}
}
/**
* Checks if the spans contain a span matching the given position.
*
* When [predicate] function is provided, for the span that is found, predicate will be called
* to further validate the span object.
*
* @param start start position of the expected span
* @param end end position of the expected span
* @param predicate function to further assert the span object
*/
fun has(start: Int, end: Int, predicate: ((T) -> Boolean)? = null) {
check("isNotNull()").that(subjects).isNotNull()
check("isNotEmpty()").that(subjects).isNotEmpty()
subjects!!.forEach { spanInfo ->
if (spanInfo.start == start && spanInfo.end == end) {
if (predicate == null || predicate.invoke(spanInfo.span)) return
}
}
failWithActual(
simpleFact(
"Can't find span $spanClazz at [$start, $end] ${toString(predicate)}"
)
)
}
/**
* Similar to [has] checks if the spans contain a span matching the given position and
* also that span is not covered by other spans.
*
* @param start start position of the expected span
* @param end end position of the expected span
* @param predicate function to further assert the span object
*/
fun hasOnTop(start: Int, end: Int, predicate: ((T) -> Boolean)? = null) {
check("isNotNull()").that(subjects).isNotNull()
check("isNotEmpty()").that(subjects).isNotEmpty()
subjects!!.reversed().forEach { spanInfo ->
if (spanInfo.start == start || spanInfo.end == end) {
// Find the target span
if (predicate == null || predicate.invoke(spanInfo.span)) return
} else if (start in spanInfo.start until spanInfo.end ||
end in (spanInfo.start + 1)..spanInfo.end
) {
// Find a span covers the given range.
// Impossible to find the target span on top.
failWithActual(simpleFact("Span not on top $spanClazz at [$start, $end]"))
}
}
failWithActual(
simpleFact(
"Can't find span $spanClazz at [$start, $end] ${toString(predicate)}"
)
)
}
private fun toString(predicate: Any?): String {
return if (predicate != null) "with predicate" else ""
}
override fun actualCustomStringRepresentation(): String {
if (subjects != null) {
return "{" +
subjects.joinToString(
separator = ", ",
transform = {
"${it.span::class.java.simpleName}[${it.start}, ${it.end}]"
}
) +
"}"
} else {
return super.actualCustomStringRepresentation()
}
}
}
internal data class SpanInfo<T : Any>(val span: T, val start: Int, val end: Int, val flags: Int)