mirror of
https://github.com/divkit/divkit.git
synced 2026-05-07 20:02:32 +00:00
Support parsing multiplatform tests
commit_hash:30c0f59f613adf7914f390c20f751136e7b34b75
This commit is contained in:
@@ -845,6 +845,8 @@
|
||||
"client/android/div-data/src/test/java/com/yandex/div/internal/parser/JsonParserTest.kt":"divkit/public/client/android/div-data/src/test/java/com/yandex/div/internal/parser/JsonParserTest.kt",
|
||||
"client/android/div-data/src/test/java/com/yandex/div/internal/parser/JsonPrinterTest.kt":"divkit/public/client/android/div-data/src/test/java/com/yandex/div/internal/parser/JsonPrinterTest.kt",
|
||||
"client/android/div-data/src/test/java/com/yandex/div/internal/parser/JsonTopologicalSortingTest.kt":"divkit/public/client/android/div-data/src/test/java/com/yandex/div/internal/parser/JsonTopologicalSortingTest.kt",
|
||||
"client/android/div-data/src/test/java/com/yandex/div/internal/parser/ParsingMultiplatformTest.kt":"divkit/public/client/android/div-data/src/test/java/com/yandex/div/internal/parser/ParsingMultiplatformTest.kt",
|
||||
"client/android/div-data/src/test/java/com/yandex/div/internal/parser/ParsingTestCase.kt":"divkit/public/client/android/div-data/src/test/java/com/yandex/div/internal/parser/ParsingTestCase.kt",
|
||||
"client/android/div-data/src/test/java/com/yandex/div/internal/util/JsonUtilsTest.kt":"divkit/public/client/android/div-data/src/test/java/com/yandex/div/internal/util/JsonUtilsTest.kt",
|
||||
"client/android/div-data/src/test/resources/com/yandex/div/core/json/cyclic-dependency-composition.json":"divkit/public/client/android/div-data/src/test/resources/com/yandex/div/core/json/cyclic-dependency-composition.json",
|
||||
"client/android/div-data/src/test/resources/com/yandex/div/core/json/cyclic-dependency-inheritance.json":"divkit/public/client/android/div-data/src/test/resources/com/yandex/div/core/json/cyclic-dependency-inheritance.json",
|
||||
|
||||
@@ -22,6 +22,9 @@ dependencies {
|
||||
|
||||
api(libs.androidx.core)
|
||||
|
||||
testImplementation(project(":test-utils"))
|
||||
|
||||
testImplementation(libs.json)
|
||||
testImplementation(libs.kotlin.reflect)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -774,7 +774,7 @@ public class JsonPropertyParser {
|
||||
@Nullable final List<V> list,
|
||||
@NonNull final Function1<V, R> converter
|
||||
) {
|
||||
if (list != null && !list.isEmpty()) {
|
||||
if (list != null) {
|
||||
int length = list.size();
|
||||
JSONArray array = new JSONArray();
|
||||
for (int i = 0; i < length; i++) {
|
||||
@@ -796,7 +796,7 @@ public class JsonPropertyParser {
|
||||
@Nullable final List<V> list,
|
||||
@NonNull final Lazy<Serializer<JSONObject, V>> serializer
|
||||
) {
|
||||
if (list != null && !list.isEmpty()) {
|
||||
if (list != null) {
|
||||
int length = list.size();
|
||||
JSONArray array = new JSONArray();
|
||||
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package com.yandex.div.internal.parser
|
||||
|
||||
import com.yandex.div.data.DivParsingEnvironment
|
||||
import com.yandex.div.internal.util.forEach
|
||||
import com.yandex.div.json.ParsingErrorLogger
|
||||
import com.yandex.div.test.crossplatform.ParsingResult
|
||||
import com.yandex.div.test.crossplatform.ParsingUtils.parseFiles
|
||||
import com.yandex.div.test.crossplatform.isForAndroid
|
||||
import com.yandex.div2.DivData
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class ParsingMultiplatformTest(testCaseParsingResult: ParsingResult<ParsingTestCase>) {
|
||||
|
||||
private val testCase = testCaseParsingResult.getOrThrow()
|
||||
private val errors = mutableListOf<Throwable>()
|
||||
|
||||
@Test
|
||||
fun run() {
|
||||
val logger = ParsingErrorLogger { errors += it }
|
||||
val env = DivParsingEnvironment(logger)
|
||||
|
||||
val divData = runCatching {
|
||||
testCase.templates?.let { env.parseTemplates(it) }
|
||||
DivData(env, testCase.card)
|
||||
}.getOrNull()
|
||||
|
||||
assertCardEquals(testCase.expectedResult.card, divData?.writeToJSON())
|
||||
Assert.assertEquals("Unexpected error count:", testCase.expectedResult.errorCount, errors.size)
|
||||
}
|
||||
|
||||
private fun assertCardEquals(expected: JSONObject?, actual: JSONObject?) {
|
||||
expected ?: return Assert.assertNull(actual)
|
||||
actual ?: return Assert.fail(
|
||||
"Failed to parse DivData. Errors: ${errors.joinToString(", ") { it.message ?: "" }}"
|
||||
)
|
||||
|
||||
runCatching {
|
||||
expected.compareValues("", actual)
|
||||
}.onFailure {
|
||||
Assert.fail(it.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.compareValues(parentPath: String, actual: JSONObject): Boolean {
|
||||
forEach { key, expectedValue: Any? ->
|
||||
val actualValue = actual.opt(key)
|
||||
val path = parentPath.takeIf { it.isNotEmpty() }?.let { "$it/$key" } ?: key
|
||||
if (!expectedValue.compare(path, actualValue)) {
|
||||
throwError(path, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Any?.compare(path: String, actual: Any?): Boolean {
|
||||
return when (this) {
|
||||
JSONObject.NULL -> actual == null || actual == this
|
||||
is JSONObject -> (actual as? JSONObject)?.let { compareValues(path, it) } ?: false
|
||||
is JSONArray -> (actual as? JSONArray)?.let { compareValues(path, it)} ?: false
|
||||
is Int -> actual == this || (actual as? Long)?.toInt() == this
|
||||
else -> actual == this
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONArray.compareValues(parentPath: String, actual: JSONArray): Boolean {
|
||||
forEach { i, expectedValue: Any? ->
|
||||
val actualValue = actual.opt(i)
|
||||
val path = parentPath.takeIf { it.isNotEmpty() }?.let { "$it/$i" } ?: "$i"
|
||||
if (!expectedValue.compare(path, actualValue)) {
|
||||
throwError(path, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun throwError(path: String, expected: Any?, actual: Any?): Nothing =
|
||||
throw Exception("DivData comparison failed for path '$path', expected='$expected', actual='$actual'")
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun cases(): List<ParsingResult<ParsingTestCase>> {
|
||||
return parseFiles("parsing_test_data") { file, jsonString ->
|
||||
val json = JSONObject(jsonString)
|
||||
if (!json.isForAndroid) return@parseFiles emptyList()
|
||||
|
||||
val result = try {
|
||||
val testCase = ParsingTestCase.from(file.name, json)
|
||||
ParsingResult.Success(testCase)
|
||||
} catch (e: Exception) {
|
||||
ParsingResult.Error(fileName = file.name, error = e)
|
||||
}
|
||||
listOf(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.yandex.div.internal.parser
|
||||
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class ParsingTestCase(
|
||||
val name: String,
|
||||
val templates: JSONObject?,
|
||||
val card: JSONObject,
|
||||
val expectedResult: ExpectedResult,
|
||||
) {
|
||||
|
||||
class ExpectedResult(
|
||||
val errorCount: Int?,
|
||||
val card: JSONObject?,
|
||||
)
|
||||
|
||||
override fun toString() = name
|
||||
|
||||
companion object {
|
||||
@Throws(JSONException::class)
|
||||
fun from(name: String, json: JSONObject): ParsingTestCase {
|
||||
val expectedResult = json.getJSONObject("expected")
|
||||
return ParsingTestCase(
|
||||
name = name,
|
||||
templates = json.optJSONObject("templates"),
|
||||
card = json.getJSONObject("card"),
|
||||
expectedResult = ExpectedResult(
|
||||
errorCount = expectedResult.optInt("error_count"),
|
||||
card = expectedResult.optJSONObject("card"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ data class ExpressionTestCase(
|
||||
val expression: String,
|
||||
val variables: List<JSONObject>,
|
||||
val functions: List<JSONObject>,
|
||||
val platform: List<String>,
|
||||
val expectedType: String,
|
||||
val expectedValue: Any,
|
||||
val expectedWarnings: List<String>,
|
||||
|
||||
-2
@@ -10,7 +10,6 @@ import com.yandex.div.json.ParsingErrorLogger
|
||||
import com.yandex.div.test.crossplatform.ParsingResult
|
||||
import com.yandex.div.test.crossplatform.isForAndroid
|
||||
import com.yandex.div.test.crossplatform.parseDateTime
|
||||
import com.yandex.div.test.crossplatform.platforms
|
||||
import com.yandex.div.test.crossplatform.toObjectList
|
||||
import com.yandex.div2.DivData
|
||||
import com.yandex.div2.DivEvaluableType
|
||||
@@ -64,7 +63,6 @@ object ExpressionTestCaseUtils {
|
||||
json.getString(CASE_EXPRESSION_VALUE_FIELD),
|
||||
json.optJSONArray(CASE_VARIABLES_FIELD).toObjectList(),
|
||||
json.optJSONArray(CASE_FUNCTIONS_FIELD).toObjectList(),
|
||||
json.platforms,
|
||||
json.getJSONObject(CASE_EXPECTED_VALUE_FIELD).type,
|
||||
json.getJSONObject(CASE_EXPECTED_VALUE_FIELD).getValue(),
|
||||
json.optJSONArray(CASE_EXPECTED_WARNINGS_FIELD)?.map { it as String } ?: emptyList(),
|
||||
|
||||
+3
-3
@@ -55,11 +55,11 @@ object ParsingUtils {
|
||||
}
|
||||
}
|
||||
|
||||
val JSONObject.platforms: List<String>
|
||||
get() = getJSONArray("platforms").map { it as String }
|
||||
val JSONObject.platforms: List<String>?
|
||||
get() = optJSONArray("platforms")?.map { it as String }
|
||||
|
||||
val JSONObject.isForAndroid: Boolean
|
||||
get() = platforms.contains("android")
|
||||
get() = platforms?.contains("android") ?: true
|
||||
|
||||
fun JSONArray?.toObjectList(): List<JSONObject> {
|
||||
if (this == null) {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"description": "Invalid item in array (transition_triggers) is ignored",
|
||||
"platforms": [
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"description": "Integer value in boolean_int property (div-text.auto_ellipsize)",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"description": "Integer value in string property is not valid (div-text.text)",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"description": "Invalid integer value in boolean_int property (div-text.auto_ellipsize)",
|
||||
"platforms": [
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"description": "Invalid number value in boolean_int property (div-text.auto_ellipsize)",
|
||||
"platforms": [
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"description": "Number value in boolean_int property (div-text.auto_ellipsize)",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"description": "Number value in integer property (div-text.font_size)",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"description": "Number value in string property is not valid (div-text.text)",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"description": "String value in boolean_int property (div-text.auto_ellipsize)",
|
||||
"platforms": [
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"description": "All properties with expressions are parsed correctly",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
@@ -14,8 +21,7 @@
|
||||
"text_color": "@{text_color}",
|
||||
"actions": [
|
||||
{
|
||||
"log_id": "test_action",
|
||||
"params": "@{action_params}"
|
||||
"log_id": "@{action_log_id}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -37,8 +43,7 @@
|
||||
"text_color": "@{text_color}",
|
||||
"actions": [
|
||||
{
|
||||
"log_id": "test_action",
|
||||
"params": "@{action_params}"
|
||||
"log_id": "@{action_log_id}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"description": "Invalid value replaced with default value",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"description": "Object with missing required property (div-action.log_id) is ignored",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"description": "All values are parsed corectly",
|
||||
"description": "All values are parsed correctly",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"unsupported_platforms": {
|
||||
"web": "There is no \"parsing\" process on the web"
|
||||
},
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
@@ -12,15 +19,15 @@
|
||||
{
|
||||
"type": "gradient",
|
||||
"colors": [
|
||||
"#112233",
|
||||
"#332211"
|
||||
"#FF112233",
|
||||
"#FF332211"
|
||||
]
|
||||
}
|
||||
],
|
||||
"font_weight": "bold",
|
||||
"font_size": 20,
|
||||
"text": "Hello!",
|
||||
"text_color": "#AABBCC",
|
||||
"text_color": "#FFAABBCC",
|
||||
"transition_triggers": [
|
||||
"data_change",
|
||||
"state_change"
|
||||
@@ -42,15 +49,15 @@
|
||||
{
|
||||
"type": "gradient",
|
||||
"colors": [
|
||||
"#112233",
|
||||
"#332211"
|
||||
"#FF112233",
|
||||
"#FF332211"
|
||||
]
|
||||
}
|
||||
],
|
||||
"font_weight": "bold",
|
||||
"font_size": 20,
|
||||
"text": "Hello!",
|
||||
"text_color": "#AABBCC",
|
||||
"text_color": "#FFAABBCC",
|
||||
"transition_triggers": [
|
||||
"data_change",
|
||||
"state_change"
|
||||
|
||||
+4
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"description": "A derived template inherits a parent template whose body contains reference definitions inside nested structures. Verifies that inherited references from the parent and the derived template's own references all resolve against the usage's reference values.",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"web"
|
||||
],
|
||||
"templates": {
|
||||
"base_text": {
|
||||
"type": "text",
|
||||
|
||||
+4
-4
@@ -27,14 +27,14 @@
|
||||
"state_id": 0,
|
||||
"div": {
|
||||
"type": "root",
|
||||
"background_color": "#F00",
|
||||
"background_color": "#FFFF0000",
|
||||
"items": [
|
||||
{
|
||||
"type": "container",
|
||||
"items": [
|
||||
{
|
||||
"type": "button",
|
||||
"background_color": "#0f0",
|
||||
"background_color": "#FF00FF00",
|
||||
"text": "SAMPLE"
|
||||
}
|
||||
]
|
||||
@@ -55,7 +55,7 @@
|
||||
"background": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "#F00"
|
||||
"color": "#FFFF0000"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
@@ -67,7 +67,7 @@
|
||||
"background": [
|
||||
{
|
||||
"type": "solid",
|
||||
"color": "#0f0"
|
||||
"color": "#FF00FF00"
|
||||
}
|
||||
],
|
||||
"text": "SAMPLE"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"description": "A template usage inside another template's body declares a reference definition whose source name matches a field on the card instance. Verifies that the reference value on the outer instance cascades through two levels of template usage to drive the inner template's field.",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"web"
|
||||
],
|
||||
"templates": {
|
||||
"inner_template": {
|
||||
"type": "image",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"description": "A reference definition lives inside a plain (non-template) nested dict embedded in a template's body — an element of an 'images' array. Verifies the reference still resolves against the outer instance despite the intermediate plain-dict layers.",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"web"
|
||||
],
|
||||
"card": {
|
||||
"log_id": "test",
|
||||
"states": [
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"description": "A single template definition reuses the same reference source name ('content') at two depths: a top-level $text and a nested $log_id inside actions[]. Verifies both sites receive the same value from the usage's reference value.",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"web"
|
||||
],
|
||||
"templates": {
|
||||
"action_text": {
|
||||
"type": "text",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"description": "A reference definition points to a source name the usage does not provide, for an optional field. Verifies an unresolved reference on an optional field is silently ignored.",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"web"
|
||||
],
|
||||
"templates": {
|
||||
"styled_text": {
|
||||
"type": "text",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"description": "A template definition declares reference definitions at multiple depths: top-level ($text), inside a nested object ($top in paddings), and inside an array element ($url in actions[]). Verifies all references resolve against the usage's reference values.",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"web"
|
||||
],
|
||||
"templates": {
|
||||
"title": {
|
||||
"type": "text",
|
||||
|
||||
Reference in New Issue
Block a user