Skip to content

Commit 0e728f9

Browse files
committed
refactor(Test): Add version-based skipping and improve test setup
1 parent cbd34ac commit 0e728f9

File tree

10 files changed

+205
-151
lines changed

10 files changed

+205
-151
lines changed

app/src/main/java/io/github/chsbuffer/revancedxposed/FingerprintCompat.kt

Lines changed: 49 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,45 +15,61 @@ private fun getTypeNameCompat(it: String): String? {
1515
else getTypeName(it)
1616
}
1717

18-
class Fingerprint(val dexkit: DexKitBridge, init: Fingerprint.() -> Unit) {
19-
var classMatcher: ClassMatcher? = null
20-
val methodMatcher = MethodMatcher()
18+
enum class AccessFlags(val modifier: Int) {
19+
PUBLIC(Modifier.PUBLIC),
20+
PRIVATE(Modifier.PRIVATE),
21+
PROTECTED(Modifier.PROTECTED),
22+
STATIC(Modifier.STATIC),
23+
FINAL(Modifier.FINAL),
24+
CONSTRUCTOR(0),
25+
}
2126

22-
init {
23-
init(this)
24-
}
27+
fun MethodMatcher.strings(vararg strings: String) {
28+
this.usingStrings(strings.toList())
29+
}
2530

26-
fun strings(vararg strings: String) {
27-
methodMatcher.usingStrings(strings.toList())
31+
fun MethodMatcher.opcodes(vararg opcodes: Opcode): OpCodesMatcher {
32+
return OpCodesMatcher(opcodes.map { it.opCode }).also {
33+
this.opCodes(it)
2834
}
35+
}
2936

30-
fun opcodes(vararg opcodes: Opcode): OpCodesMatcher {
31-
return OpCodesMatcher(opcodes.map { it.opCode }).also {
32-
methodMatcher.opCodes(it)
33-
}
37+
fun MethodMatcher.accessFlags(vararg accessFlags: AccessFlags) {
38+
val modifiers = accessFlags.map { it.modifier }.reduce { acc, next -> acc or next }
39+
if (modifiers != 0) this.modifiers(modifiers)
40+
if (accessFlags.contains(AccessFlags.CONSTRUCTOR)) {
41+
if (accessFlags.contains(AccessFlags.STATIC)) this.name = "<clinit>"
42+
else this.name = "<init>"
3443
}
44+
}
3545

36-
fun accessFlags(vararg accessFlags: AccessFlags) {
37-
val modifiers = accessFlags.map { it.modifier }.reduce { acc, next -> acc or next }
38-
if (modifiers != 0) methodMatcher.modifiers(modifiers)
39-
if (accessFlags.contains(AccessFlags.CONSTRUCTOR)) {
40-
if (accessFlags.contains(AccessFlags.STATIC)) methodMatcher.name = "<clinit>"
41-
else methodMatcher.name = "<init>"
42-
}
43-
}
46+
fun MethodMatcher.parameters(vararg parameters: String) {
47+
this.paramTypes(parameters.map(::getTypeNameCompat))
48+
}
4449

45-
fun parameters(vararg parameters: String) {
46-
methodMatcher.paramTypes(parameters.map(::getTypeNameCompat))
47-
}
50+
fun MethodMatcher.returns(returnType: String) {
51+
getTypeNameCompat(returnType)?.let { this.returnType = it }
52+
}
4853

49-
fun returns(returnType: String) {
50-
getTypeNameCompat(returnType)?.let { methodMatcher.returnType = it }
51-
}
54+
fun MethodMatcher.literal(literalSupplier: () -> Number) {
55+
this.usingNumbers(literalSupplier())
56+
}
57+
58+
class Fingerprint(val dexkit: DexKitBridge, init: Fingerprint.() -> Unit) {
59+
var classMatcher: ClassMatcher? = null
60+
val methodMatcher = MethodMatcher()
5261

53-
fun literal(literalSupplier: () -> Number) {
54-
methodMatcher.usingNumbers(literalSupplier())
62+
init {
63+
init(this)
5564
}
5665

66+
fun strings(vararg strings: String) = methodMatcher.strings(*strings)
67+
fun opcodes(vararg opcodes: Opcode) = methodMatcher.opcodes(*opcodes)
68+
fun accessFlags(vararg accessFlags: AccessFlags) = methodMatcher.accessFlags(*accessFlags)
69+
fun parameters(vararg parameters: String) = methodMatcher.parameters(*parameters)
70+
fun returns(returnType: String) = methodMatcher.returns(returnType)
71+
fun literal(literalSupplier: () -> Number) = methodMatcher.literal(literalSupplier)
72+
5773
/*
5874
* dexkit method matcher
5975
* */
@@ -93,6 +109,7 @@ interface ResourceFinder {
93109

94110
lateinit var resourceMappings: ResourceFinder
95111

112+
typealias FindFunc = DexKitBridge.() -> Any
96113
typealias FindClassFunc = DexKitBridge.() -> ClassData
97114
typealias FindMethodFunc = DexKitBridge.() -> MethodData
98115
typealias FindMethodListFunc = DexKitBridge.() -> List<MethodData>
@@ -102,49 +119,7 @@ fun fingerprint(block: Fingerprint.() -> Unit): FindMethodFunc {
102119
return { Fingerprint(this, block).run() }
103120
}
104121

105-
fun findMethodDirect(block: FindMethodFunc): FindMethodFunc = block
106-
107-
fun findMethodListDirect(block: FindMethodListFunc): FindMethodListFunc = block
108-
109-
fun findClassDirect(block: FindClassFunc): FindClassFunc = block
110-
111-
fun findFieldDirect(block: FindFieldFunc): FindFieldFunc = block
112-
113-
fun MethodMatcher.strings(vararg strings: String) {
114-
this.usingStrings(strings.toList())
115-
}
116-
117-
fun MethodMatcher.opcodes(vararg opcodes: Opcode): OpCodesMatcher {
118-
return OpCodesMatcher(opcodes.map { it.opCode }).also {
119-
this.opCodes(it)
120-
}
121-
}
122-
123-
enum class AccessFlags(val modifier: Int) {
124-
PUBLIC(Modifier.PUBLIC),
125-
PRIVATE(Modifier.PRIVATE),
126-
PROTECTED(Modifier.PROTECTED),
127-
STATIC(Modifier.STATIC),
128-
FINAL(Modifier.FINAL),
129-
CONSTRUCTOR(0),
130-
}
131-
132-
fun MethodMatcher.accessFlags(vararg accessFlags: AccessFlags) {
133-
this.modifiers(accessFlags.map { it.modifier }.reduce { acc, next -> acc or next })
134-
if (accessFlags.contains(AccessFlags.CONSTRUCTOR)) {
135-
if (accessFlags.contains(AccessFlags.STATIC)) this.name = "<clinit>"
136-
else this.name = "<init>"
137-
}
138-
}
139-
140-
fun MethodMatcher.parameters(vararg parameters: String) {
141-
this.paramTypes(parameters.map(::getTypeNameCompat))
142-
}
143-
144-
fun MethodMatcher.returns(returnType: String) {
145-
getTypeNameCompat(returnType)?.let { this.returnType = it }
146-
}
147-
148-
fun MethodMatcher.literal(literalSupplier: () -> Number) {
149-
this.usingNumbers(literalSupplier())
150-
}
122+
fun findMethodDirect(block: FindMethodFunc) = block
123+
fun findMethodListDirect(block: FindMethodListFunc) = block
124+
fun findClassDirect(block: FindClassFunc) = block
125+
fun findFieldDirect(block: FindFieldFunc) = block

app/src/main/java/io/github/chsbuffer/revancedxposed/UnitTestAnnotations.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,56 @@ package io.github.chsbuffer.revancedxposed
44
// Use with caution!!!
55
@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FUNCTION)
66
annotation class SkipTest()
7+
8+
class AppVersion(val versionString: String) : Comparable<AppVersion> {
9+
init {
10+
require(versionString.matches(Regex("\\d+\\.\\d+(\\.\\d+)?"))) {
11+
"Version string must be in the format major.minor[.patch] (e.g., 1.2 or 1.2.3)"
12+
}
13+
}
14+
15+
private val parts: List<Int> by lazy { versionString.split('.').map { it.toInt() } }
16+
17+
val major: Int
18+
get() = parts[0]
19+
20+
val minor: Int
21+
get() = parts[1]
22+
23+
val patch: Int
24+
get() = if (parts.size > 2) parts[2] else 0
25+
26+
override fun compareTo(other: AppVersion): Int {
27+
if (this.major != other.major) {
28+
return this.major.compareTo(other.major)
29+
}
30+
if (this.minor != other.minor) {
31+
return this.minor.compareTo(other.minor)
32+
}
33+
return this.patch.compareTo(other.patch)
34+
}
35+
36+
override fun toString(): String = versionString
37+
}
38+
39+
// Skip Unit Test by version constraint
40+
@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FUNCTION)
41+
annotation class RequireAppVersion(
42+
val minVersion: String = "",
43+
val maxVersion: String = ""
44+
)
45+
46+
class VersionConstraintFailedException(message: String) : Exception(message)
47+
48+
fun match(appVersion: AppVersion, minVersionStr: String, maxVersionStr: String) {
49+
val minVersion = minVersionStr.takeIf { it.isNotEmpty() }?.let { AppVersion(it) }
50+
val maxVersion = maxVersionStr.takeIf { it.isNotEmpty() }?.let { AppVersion(it) }
51+
when {
52+
minVersion == null && maxVersion == null -> return // No version constraint
53+
minVersion != null && appVersion < minVersion ->
54+
throw VersionConstraintFailedException("Min version mismatch (current: $appVersion, required: $minVersion)")
55+
56+
maxVersion != null && appVersion > maxVersion ->
57+
throw VersionConstraintFailedException("Max version mismatch (current: $appVersion, required: $maxVersion)")
58+
}
59+
}

app/src/main/java/io/github/chsbuffer/revancedxposed/youtube/misc/backgroundplayback/Fingerprints.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package io.github.chsbuffer.revancedxposed.youtube.misc.backgroundplayback
22

33
import io.github.chsbuffer.revancedxposed.AccessFlags
44
import io.github.chsbuffer.revancedxposed.Opcode
5-
import io.github.chsbuffer.revancedxposed.SkipTest
5+
import io.github.chsbuffer.revancedxposed.RequireAppVersion
66
import io.github.chsbuffer.revancedxposed.findMethodDirect
77
import io.github.chsbuffer.revancedxposed.fingerprint
88
import io.github.chsbuffer.revancedxposed.resourceMappings
@@ -96,7 +96,7 @@ val shortsBackgroundPlaybackFeatureFlagFingerprint = fingerprint {
9696
internal const val PIP_INPUT_CONSUMER_FEATURE_FLAG = 45638483L
9797

9898
// Fix 'E/InputDispatcher: Window handle pip_input_consumer has no registered input channel'
99-
@get:SkipTest
99+
@get:RequireAppVersion("19.34")
100100
val pipInputConsumerFeatureFlagFingerprint = fingerprint {
101101
literal { PIP_INPUT_CONSUMER_FEATURE_FLAG}
102102
}

app/src/main/java/io/github/chsbuffer/revancedxposed/youtube/misc/litho/filter/Fingerprints.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package io.github.chsbuffer.revancedxposed.youtube.misc.litho.filter
22

33
import io.github.chsbuffer.revancedxposed.AccessFlags
44
import io.github.chsbuffer.revancedxposed.Opcode
5-
import io.github.chsbuffer.revancedxposed.SkipTest
5+
import io.github.chsbuffer.revancedxposed.RequireAppVersion
66
import io.github.chsbuffer.revancedxposed.findClassDirect
77
import io.github.chsbuffer.revancedxposed.findFieldDirect
88
import io.github.chsbuffer.revancedxposed.findMethodDirect
@@ -50,7 +50,7 @@ val lithoThreadExecutorFingerprint = fingerprint {
5050
literal { 1L }
5151
}
5252

53-
@get:SkipTest
53+
@get:RequireAppVersion("19.25", "20.04.99")
5454
val lithoComponentNameUpbFeatureFlagFingerprint = fingerprint {
5555
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
5656
returns("Z")

app/src/main/java/io/github/chsbuffer/revancedxposed/youtube/video/information/Fingerprints.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import io.github.chsbuffer.revancedxposed.findClassDirect
77
import io.github.chsbuffer.revancedxposed.findFieldDirect
88
import io.github.chsbuffer.revancedxposed.findMethodDirect
99
import io.github.chsbuffer.revancedxposed.fingerprint
10-
import io.github.chsbuffer.revancedxposed.parameters
1110
import io.github.chsbuffer.revancedxposed.youtube.shared.videoQualityChangedFingerprint
1211
import org.luckypray.dexkit.query.enums.OpCodeMatchType
1312
import org.luckypray.dexkit.query.enums.UsingType

app/src/main/java/io/github/chsbuffer/revancedxposed/youtube/video/speed/custom/Fingerprints.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package io.github.chsbuffer.revancedxposed.youtube.video.speed.custom
22

33
import io.github.chsbuffer.revancedxposed.AccessFlags
44
import io.github.chsbuffer.revancedxposed.Opcode
5-
import io.github.chsbuffer.revancedxposed.SkipTest
5+
import io.github.chsbuffer.revancedxposed.RequireAppVersion
66
import io.github.chsbuffer.revancedxposed.findMethodDirect
77
import io.github.chsbuffer.revancedxposed.fingerprint
88
import io.github.chsbuffer.revancedxposed.parameters
@@ -77,7 +77,7 @@ val getPlaybackSpeedMethodReference = findMethodDirect {
7777
}.single()
7878
}
7979

80-
@get:SkipTest
80+
@get:RequireAppVersion("19.25")
8181
val onSpeedTapAndHoldFingerprint = findMethodDirect {
8282
findMethod {
8383
matcher {

app/src/test/java/io/github/chsbuffer/revancedxposed/FingerprintsKtTest.kt

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.github.chsbuffer.revancedxposed
22

3-
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
43
import org.junit.jupiter.api.Assertions.assertTrue
54
import org.junit.jupiter.api.DynamicTest
65
import org.junit.jupiter.api.TestFactory
@@ -12,38 +11,59 @@ import java.nio.file.Path
1211
import kotlin.io.path.Path
1312
import kotlin.io.path.invariantSeparatorsPathString
1413
import kotlin.io.path.name
15-
import kotlin.time.measureTime
14+
import kotlin.system.measureTimeMillis
1615

1716
@ParameterizedClass
1817
@ArgumentsSource(FilePathArgumentsProvider::class)
1918
class FingerprintsKtTest(val apkPath: Path) {
19+
init {
20+
TestSetup.setupForApk(apkPath.toString())
21+
}
2022

21-
val dexkit: DexKitBridge = TestSetup.getDexKit(apkPath.toString())
23+
val dexkit: DexKitBridge = TestSetup.dexkit.get()!!
24+
val appVersion: AppVersion = TestSetup.appVersion.get()!!
2225

2326
fun testFingerprints(clazz: Class<*>) {
24-
clazz.methods.forEach { method ->
25-
if (method.isAnnotationPresent(SkipTest::class.java)) return@forEach
26-
if (!method.isStatic) return@forEach
27-
28-
val func = method(null) as? (DexKitBridge) -> Any ?: return@forEach
29-
print("${method.name.drop(3)}: ")
30-
assertDoesNotThrow {
31-
measureTime {
32-
val value = func(dexkit)
33-
if (value is List<*>) {
34-
assertTrue(value.isNotEmpty())
35-
print(value.joinToString(", "))
36-
} else {
37-
print("$value")
27+
val errors = mutableListOf<Throwable>()
28+
clazz.methods.asSequence()
29+
.filter { it.isStatic }
30+
.filter { !it.isAnnotationPresent(SkipTest::class.java) }
31+
.forEach { method ->
32+
val func = method(null) as? FindFunc ?: return@forEach
33+
val methodName = method.name.drop(3)
34+
print("$methodName: ")
35+
method.getAnnotation(RequireAppVersion::class.java)?.also { anno ->
36+
try {
37+
match(appVersion, anno.minVersion, anno.maxVersion)
38+
} catch (e: VersionConstraintFailedException) {
39+
System.out.flush()
40+
System.err.println("Skipping: ${e.message}")
41+
return@forEach
3842
}
39-
}.let {
40-
if (it.inWholeMilliseconds > 20)
41-
println(", slow match: $it")
42-
else
43-
println()
43+
}
44+
try {
45+
val time = measureTimeMillis {
46+
val value = func(dexkit)
47+
if (value is List<*>) {
48+
assertTrue(value.isNotEmpty())
49+
print(value.joinToString(", "))
50+
} else {
51+
print("$value")
52+
}
53+
}
54+
if (time > 20) {
55+
print(", slow match: ${time}ms")
56+
}
57+
println()
58+
} catch (e: Throwable) {
59+
println()
60+
errors.add(e)
61+
System.err.println(e.stackTraceToString())
4462
}
4563
}
46-
}
64+
65+
if (errors.isNotEmpty())
66+
throw AssertionError()
4767
}
4868

4969
@TestFactory
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.github.chsbuffer.revancedxposed
2+
3+
import jadx.api.JadxDecompiler
4+
import jadx.api.plugins.input.data.attributes.JadxAttrType
5+
6+
class JadxResourceReader(val jadx: JadxDecompiler) {
7+
operator fun get(type: String, name: String): Int {
8+
val typeClass = jadx.root.appResClass!!.innerClasses.first { it.name == type }
9+
val field = typeClass.fields.first { it.name == name }
10+
return field.get(JadxAttrType.CONSTANT_VALUE).value as Int
11+
}
12+
}

0 commit comments

Comments
 (0)