|
| 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 | + path: MutableProtoValuePath = mutableListOf(), |
| 91 | + differences: MutableList<DifferencePathPair<*>> = mutableListOf(), |
| 92 | +): MutableList<DifferencePathPair<*>> { |
| 93 | + val map1 = struct1.fieldsMap |
| 94 | + val map2 = struct2.fieldsMap |
| 95 | + |
| 96 | + map1.entries.forEach { (key, value) -> |
| 97 | + if (key !in map2) { |
| 98 | + differences.add(path, Difference.StructMissingKey(key, value)) |
| 99 | + } else { |
| 100 | + path.withAppendedStructKey(key) { valueDiff(value, map2[key]!!, path, differences) } |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + map2.entries.forEach { (key, value) -> |
| 105 | + if (key !in map1) { |
| 106 | + differences.add(path, Difference.StructUnexpectedKey(key, value)) |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + return differences |
| 111 | +} |
| 112 | + |
| 113 | +fun listValueDiff( |
| 114 | + listValue1: ListValue, |
| 115 | + listValue2: ListValue, |
| 116 | + path: MutableProtoValuePath = mutableListOf(), |
| 117 | + differences: MutableList<DifferencePathPair<*>> = mutableListOf(), |
| 118 | +): MutableList<DifferencePathPair<*>> { |
| 119 | + repeat(listValue1.valuesCount.coerceAtMost(listValue2.valuesCount)) { |
| 120 | + val value1 = listValue1.getValues(it) |
| 121 | + val value2 = listValue2.getValues(it) |
| 122 | + path.withAppendedListIndex(it) { valueDiff(value1, value2, path, differences) } |
| 123 | + } |
| 124 | + |
| 125 | + if (listValue1.valuesCount > listValue2.valuesCount) { |
| 126 | + (listValue2.valuesCount until listValue1.valuesCount).forEach { |
| 127 | + differences.add(path, Difference.ListMissingElement(it, listValue1.getValues(it))) |
| 128 | + } |
| 129 | + } else if (listValue1.valuesCount < listValue2.valuesCount) { |
| 130 | + (listValue1.valuesCount until listValue2.valuesCount).forEach { |
| 131 | + differences.add(path, Difference.ListUnexpectedElement(it, listValue2.getValues(it))) |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + return differences |
| 136 | +} |
| 137 | + |
| 138 | +fun valueDiff( |
| 139 | + value1: Value, |
| 140 | + value2: Value, |
| 141 | + path: MutableProtoValuePath = mutableListOf(), |
| 142 | + differences: MutableList<DifferencePathPair<*>> = mutableListOf(), |
| 143 | +): MutableList<DifferencePathPair<*>> { |
| 144 | + if (value1.kindCase != value2.kindCase) { |
| 145 | + differences.add(path, Difference.KindCase(value1, value2)) |
| 146 | + return differences |
| 147 | + } |
| 148 | + |
| 149 | + when (value1.kindCase) { |
| 150 | + Value.KindCase.KIND_NOT_SET, |
| 151 | + Value.KindCase.NULL_VALUE -> {} |
| 152 | + Value.KindCase.STRUCT_VALUE -> |
| 153 | + structDiff(value1.structValue, value2.structValue, path, differences) |
| 154 | + Value.KindCase.LIST_VALUE -> |
| 155 | + listValueDiff(value1.listValue, value2.listValue, path, differences) |
| 156 | + Value.KindCase.BOOL_VALUE -> |
| 157 | + if (value1.boolValue != value2.boolValue) { |
| 158 | + differences.add(path, Difference.BoolValue(value1.boolValue, value2.boolValue)) |
| 159 | + } |
| 160 | + Value.KindCase.NUMBER_VALUE -> |
| 161 | + if (!numberValuesEqual(value1.numberValue, value2.numberValue)) { |
| 162 | + differences.add(path, Difference.NumberValue(value1.numberValue, value2.numberValue)) |
| 163 | + } |
| 164 | + Value.KindCase.STRING_VALUE -> |
| 165 | + if (value1.stringValue != value2.stringValue) { |
| 166 | + differences.add(path, Difference.StringValue(value1.stringValue, value2.stringValue)) |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + return differences |
| 171 | +} |
| 172 | + |
| 173 | +private fun MutableCollection<DifferencePathPair<*>>.add( |
| 174 | + path: MutableProtoValuePath, |
| 175 | + difference: Difference |
| 176 | +) { |
| 177 | + add(DifferencePathPair(path.toList(), difference)) |
| 178 | +} |
| 179 | + |
| 180 | +fun Collection<DifferencePathPair<*>>.toSummaryString(): String = buildString { |
| 181 | + val differences: Collection<DifferencePathPair<*>> = this@toSummaryString |
| 182 | + if (differences.size == 1) { |
| 183 | + append("1 difference: ") |
| 184 | + append(differences.single().run { "${path.toPathString()}=$difference" }) |
| 185 | + } else { |
| 186 | + append("${differences.size} differences:") |
| 187 | + differences.forEachIndexed { index, (path, difference) -> |
| 188 | + append('\n') |
| 189 | + append(index + 1) |
| 190 | + append(": ") |
| 191 | + appendPathString(path) |
| 192 | + append('=') |
| 193 | + append(difference) |
| 194 | + } |
| 195 | + } |
| 196 | +} |
| 197 | + |
| 198 | +fun numberValuesEqual(value1: Double, value2: Double): Boolean = |
| 199 | + if (value1.isNaN()) { |
| 200 | + value2.isNaN() |
| 201 | + } else if (value1 != value2) { |
| 202 | + false |
| 203 | + } else if (value1 == 0.0) { |
| 204 | + // Explicitly consider 0.0 and -0.0 to be "unequal"; the == operator considers them "equal". |
| 205 | + value1.toBits() == value2.toBits() |
| 206 | + } else { |
| 207 | + true |
| 208 | + } |
0 commit comments