|
| 1 | +/* |
| 2 | + * Copyright 2025 Google LLC |
| 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 com.google.firebase.dataconnect.testutil |
| 18 | + |
| 19 | +import com.google.protobuf.ListValue |
| 20 | +import com.google.protobuf.Struct |
| 21 | +import com.google.protobuf.Value |
| 22 | + |
| 23 | +fun structFastEqual(struct1: Struct, struct2: Struct): Boolean { |
| 24 | + if (struct1 === struct2) { |
| 25 | + return true |
| 26 | + } else if (struct1.fieldsCount != struct2.fieldsCount) { |
| 27 | + return false |
| 28 | + } |
| 29 | + |
| 30 | + val struct2FieldsMap = struct2.fieldsMap |
| 31 | + struct1.fieldsMap.entries.forEach { (key, value1) -> |
| 32 | + val value2 = struct2FieldsMap[key] ?: return false |
| 33 | + if (!valueFastEqual(value1, value2)) { |
| 34 | + return false |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + return true |
| 39 | +} |
| 40 | + |
| 41 | +fun listValueFastEqual(listValue1: ListValue, listValue2: ListValue): Boolean { |
| 42 | + if (listValue1 === listValue2) { |
| 43 | + return true |
| 44 | + } else if (listValue1.valuesCount != listValue2.valuesCount) { |
| 45 | + return false |
| 46 | + } |
| 47 | + |
| 48 | + listValue1.valuesList.zip(listValue2.valuesList).forEach { (value1, value2) -> |
| 49 | + if (!valueFastEqual(value1, value2)) { |
| 50 | + return false |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + return true |
| 55 | +} |
| 56 | + |
| 57 | +fun valueFastEqual(value1: Value, value2: Value): Boolean { |
| 58 | + if (value1 === value2) { |
| 59 | + return true |
| 60 | + } else if (value1.kindCase != value2.kindCase) { |
| 61 | + return false |
| 62 | + } |
| 63 | + return when (value1.kindCase) { |
| 64 | + Value.KindCase.KIND_NOT_SET -> true |
| 65 | + Value.KindCase.NULL_VALUE -> true |
| 66 | + Value.KindCase.NUMBER_VALUE -> numberValuesEqual(value1.numberValue, value2.numberValue) |
| 67 | + Value.KindCase.STRING_VALUE -> value1.stringValue == value2.stringValue |
| 68 | + Value.KindCase.BOOL_VALUE -> value1.boolValue == value2.boolValue |
| 69 | + Value.KindCase.STRUCT_VALUE -> structFastEqual(value1.structValue, value2.structValue) |
| 70 | + Value.KindCase.LIST_VALUE -> listValueFastEqual(value1.listValue, value2.listValue) |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +data class DifferencePathPair<T : Difference>(val path: ProtoValuePath, val difference: T) |
| 75 | + |
| 76 | +sealed interface Difference { |
| 77 | + data class KindCase(val value1: Value, val value2: Value) : Difference |
| 78 | + data class BoolValue(val value1: Boolean, val value2: Boolean) : Difference |
| 79 | + data class NumberValue(val value1: Double, val value2: Double) : Difference |
| 80 | + data class StringValue(val value1: String, val value2: String) : Difference |
| 81 | + data class StructMissingKey(val key: String, val value: Value) : Difference |
| 82 | + data class StructUnexpectedKey(val key: String, val value: Value) : Difference |
| 83 | + data class ListMissingElement(val index: Int, val value: Value) : Difference |
| 84 | + data class ListUnexpectedElement(val index: Int, val value: Value) : Difference |
| 85 | +} |
| 86 | + |
| 87 | +fun structDiff( |
| 88 | + struct1: Struct, |
| 89 | + struct2: Struct, |
| 90 | + differences: DifferenceAccumulator = DifferenceAccumulator(), |
| 91 | +): DifferenceAccumulator { |
| 92 | + val map1 = struct1.fieldsMap |
| 93 | + val map2 = struct2.fieldsMap |
| 94 | + |
| 95 | + map1.entries.forEach { (key, value) -> |
| 96 | + if (key !in map2) { |
| 97 | + differences.add(Difference.StructMissingKey(key, value)) |
| 98 | + } else { |
| 99 | + differences.withPushedPathComponent({ ProtoValuePathComponent.StructKey(key) }) { |
| 100 | + valueDiff(value, map2[key]!!, differences) |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + map2.entries.forEach { (key, value) -> |
| 106 | + if (key !in map1) { |
| 107 | + differences.add(Difference.StructUnexpectedKey(key, value)) |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + return differences |
| 112 | +} |
| 113 | + |
| 114 | +fun listValueDiff( |
| 115 | + listValue1: ListValue, |
| 116 | + listValue2: ListValue, |
| 117 | + differences: DifferenceAccumulator = DifferenceAccumulator(), |
| 118 | +): DifferenceAccumulator { |
| 119 | + repeat(listValue1.valuesCount.coerceAtMost(listValue2.valuesCount)) { |
| 120 | + val value1 = listValue1.getValues(it) |
| 121 | + val value2 = listValue2.getValues(it) |
| 122 | + differences.withPushedPathComponent({ ProtoValuePathComponent.ListIndex(it) }) { |
| 123 | + valueDiff(value1, value2, differences) |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + if (listValue1.valuesCount > listValue2.valuesCount) { |
| 128 | + (listValue2.valuesCount until listValue1.valuesCount).forEach { |
| 129 | + differences.add(Difference.ListMissingElement(it, listValue1.getValues(it))) |
| 130 | + } |
| 131 | + } else if (listValue1.valuesCount < listValue2.valuesCount) { |
| 132 | + (listValue1.valuesCount until listValue2.valuesCount).forEach { |
| 133 | + differences.add(Difference.ListUnexpectedElement(it, listValue2.getValues(it))) |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + return differences |
| 138 | +} |
| 139 | + |
| 140 | +fun valueDiff( |
| 141 | + value1: Value, |
| 142 | + value2: Value, |
| 143 | + differences: DifferenceAccumulator = DifferenceAccumulator(), |
| 144 | +): DifferenceAccumulator { |
| 145 | + if (value1.kindCase != value2.kindCase) { |
| 146 | + differences.add(Difference.KindCase(value1, value2)) |
| 147 | + return differences |
| 148 | + } |
| 149 | + |
| 150 | + when (value1.kindCase) { |
| 151 | + Value.KindCase.KIND_NOT_SET, |
| 152 | + Value.KindCase.NULL_VALUE -> {} |
| 153 | + Value.KindCase.STRUCT_VALUE -> structDiff(value1.structValue, value2.structValue, differences) |
| 154 | + Value.KindCase.LIST_VALUE -> listValueDiff(value1.listValue, value2.listValue, differences) |
| 155 | + Value.KindCase.BOOL_VALUE -> |
| 156 | + if (value1.boolValue != value2.boolValue) { |
| 157 | + differences.add(Difference.BoolValue(value1.boolValue, value2.boolValue)) |
| 158 | + } |
| 159 | + Value.KindCase.NUMBER_VALUE -> |
| 160 | + if (!numberValuesEqual(value1.numberValue, value2.numberValue)) { |
| 161 | + differences.add(Difference.NumberValue(value1.numberValue, value2.numberValue)) |
| 162 | + } |
| 163 | + Value.KindCase.STRING_VALUE -> |
| 164 | + if (value1.stringValue != value2.stringValue) { |
| 165 | + differences.add(Difference.StringValue(value1.stringValue, value2.stringValue)) |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + return differences |
| 170 | +} |
| 171 | + |
| 172 | +class DifferenceAccumulator { |
| 173 | + private val differences = mutableListOf<DifferencePathPair<*>>() |
| 174 | + private val path: MutableProtoValuePath = mutableListOf() |
| 175 | + |
| 176 | + val size: Int by differences::size |
| 177 | + |
| 178 | + fun toList(): List<DifferencePathPair<*>> = differences.toList() |
| 179 | + |
| 180 | + fun pushPathComponent(pathComponent: ProtoValuePathComponent) { |
| 181 | + path.add(pathComponent) |
| 182 | + } |
| 183 | + |
| 184 | + fun popPathComponent() { |
| 185 | + path.removeAt(path.lastIndex) |
| 186 | + } |
| 187 | + |
| 188 | + fun add(difference: Difference) { |
| 189 | + differences.add(DifferencePathPair(path.toList(), difference)) |
| 190 | + } |
| 191 | + |
| 192 | + override fun toString() = buildString { |
| 193 | + if (differences.size == 1) { |
| 194 | + append("1 difference: ") |
| 195 | + append(differences.single().run { "${path.toPathString()}=$difference" }) |
| 196 | + } else { |
| 197 | + append("${differences.size} differences:") |
| 198 | + differences.forEachIndexed { index, (path, difference) -> |
| 199 | + append('\n') |
| 200 | + append(index + 1) |
| 201 | + append(": ") |
| 202 | + appendPathString(path) |
| 203 | + append('=') |
| 204 | + append(difference) |
| 205 | + } |
| 206 | + } |
| 207 | + } |
| 208 | +} |
| 209 | + |
| 210 | +private inline fun <T> DifferenceAccumulator?.withPushedPathComponent( |
| 211 | + pathComponent: () -> ProtoValuePathComponent, |
| 212 | + block: () -> T |
| 213 | +): T { |
| 214 | + this?.pushPathComponent(pathComponent()) |
| 215 | + return try { |
| 216 | + block() |
| 217 | + } finally { |
| 218 | + this?.popPathComponent() |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +fun numberValuesEqual(value1: Double, value2: Double): Boolean = |
| 223 | + if (value1.isNaN()) { |
| 224 | + value2.isNaN() |
| 225 | + } else if (value1 != value2) { |
| 226 | + false |
| 227 | + } else if (value1 == 0.0) { |
| 228 | + // Explicitly consider 0.0 and -0.0 to be "unequal"; the == operator considers them "equal". |
| 229 | + value1.toBits() == value2.toBits() |
| 230 | + } else { |
| 231 | + true |
| 232 | + } |
0 commit comments