Skip to content

Commit f5fdcf6

Browse files
committed
Test offline mirroring semantics
1 parent 6332029 commit f5fdcf6

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

firebase-firestore/firebase-firestore.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ dependencies {
142142
implementation libs.grpc.stub
143143
implementation libs.kotlin.stdlib
144144
implementation libs.kotlinx.coroutines.core
145+
implementation 'com.google.re2j:re2j:1.6'
145146

146147
compileOnly libs.autovalue.annotations
147148
compileOnly libs.javax.annotation.jsr250
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.google.firebase.firestore.pipeline
2+
3+
import com.google.firebase.firestore.pipeline.Expr.Companion.add
4+
import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContains
5+
import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContainsAll
6+
import com.google.firebase.firestore.pipeline.Expr.Companion.arrayContainsAny
7+
import com.google.firebase.firestore.pipeline.Expr.Companion.arrayLength
8+
import com.google.firebase.firestore.pipeline.Expr.Companion.byteLength
9+
import com.google.firebase.firestore.pipeline.Expr.Companion.charLength
10+
import com.google.firebase.firestore.pipeline.Expr.Companion.constant
11+
import com.google.firebase.firestore.pipeline.Expr.Companion.divide
12+
import com.google.firebase.firestore.pipeline.Expr.Companion.endsWith
13+
import com.google.firebase.firestore.pipeline.Expr.Companion.eq
14+
import com.google.firebase.firestore.pipeline.Expr.Companion.eqAny
15+
import com.google.firebase.firestore.pipeline.Expr.Companion.field
16+
import com.google.firebase.firestore.pipeline.Expr.Companion.gt
17+
import com.google.firebase.firestore.pipeline.Expr.Companion.gte
18+
import com.google.firebase.firestore.pipeline.Expr.Companion.isNan
19+
import com.google.firebase.firestore.pipeline.Expr.Companion.isNotNan
20+
import com.google.firebase.firestore.pipeline.Expr.Companion.like
21+
import com.google.firebase.firestore.pipeline.Expr.Companion.lt
22+
import com.google.firebase.firestore.pipeline.Expr.Companion.lte
23+
import com.google.firebase.firestore.pipeline.Expr.Companion.mod
24+
import com.google.firebase.firestore.pipeline.Expr.Companion.multiply
25+
import com.google.firebase.firestore.pipeline.Expr.Companion.neq
26+
import com.google.firebase.firestore.pipeline.Expr.Companion.notEqAny
27+
import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue
28+
import com.google.firebase.firestore.pipeline.Expr.Companion.regexContains
29+
import com.google.firebase.firestore.pipeline.Expr.Companion.regexMatch
30+
import com.google.firebase.firestore.pipeline.Expr.Companion.reverse
31+
import com.google.firebase.firestore.pipeline.Expr.Companion.startsWith
32+
import com.google.firebase.firestore.pipeline.Expr.Companion.strConcat
33+
import com.google.firebase.firestore.pipeline.Expr.Companion.strContains
34+
import com.google.firebase.firestore.pipeline.Expr.Companion.subtract
35+
import com.google.firebase.firestore.pipeline.Expr.Companion.timestampToUnixMicros
36+
import com.google.firebase.firestore.pipeline.Expr.Companion.timestampToUnixMillis
37+
import com.google.firebase.firestore.pipeline.Expr.Companion.timestampToUnixSeconds
38+
import com.google.firebase.firestore.pipeline.Expr.Companion.toLower
39+
import com.google.firebase.firestore.pipeline.Expr.Companion.toUpper
40+
import com.google.firebase.firestore.pipeline.Expr.Companion.trim
41+
import com.google.firebase.firestore.pipeline.Expr.Companion.unixMicrosToTimestamp
42+
import com.google.firebase.firestore.pipeline.Expr.Companion.unixMillisToTimestamp
43+
import com.google.firebase.firestore.pipeline.Expr.Companion.unixSecondsToTimestamp
44+
import org.junit.Test
45+
import org.junit.runner.RunWith
46+
import org.robolectric.RobolectricTestRunner
47+
48+
@RunWith(RobolectricTestRunner::class)
49+
internal class MirroringSemanticsTests {
50+
51+
private val NULL_INPUT = nullValue()
52+
// Error: Integer division by zero
53+
private val ERROR_INPUT = divide(constant(1L), constant(0L))
54+
// Unset: Field that doesn't exist in the default test document
55+
private val UNSET_INPUT = field("non-existent-field")
56+
// Valid: A simple valid input for binary tests
57+
private val VALID_INPUT = constant(42L)
58+
59+
private enum class ExpectedOutcome {
60+
NULL,
61+
ERROR
62+
}
63+
64+
private data class UnaryTestCase(
65+
val inputExpr: Expr,
66+
val expectedOutcome: ExpectedOutcome,
67+
val description: String
68+
)
69+
70+
private data class BinaryTestCase(
71+
val left: Expr,
72+
val right: Expr,
73+
val expectedOutcome: ExpectedOutcome,
74+
val description: String
75+
)
76+
77+
@Test
78+
fun `unary function input mirroring`() {
79+
val unaryFunctionBuilders =
80+
listOf<Pair<String, (Expr) -> Expr>>(
81+
"isNan" to { v -> isNan(v) },
82+
"isNotNan" to { v -> isNotNan(v) },
83+
"arrayLength" to { v -> arrayLength(v) },
84+
"reverse" to { v -> reverse(v) },
85+
"charLength" to { v -> charLength(v) },
86+
"byteLength" to { v -> byteLength(v) },
87+
"toLower" to { v -> toLower(v) },
88+
"toUpper" to { v -> toUpper(v) },
89+
"trim" to { v -> trim(v) },
90+
"unixMicrosToTimestamp" to { v -> unixMicrosToTimestamp(v) },
91+
"timestampToUnixMicros" to { v -> timestampToUnixMicros(v) },
92+
"unixMillisToTimestamp" to { v -> unixMillisToTimestamp(v) },
93+
"timestampToUnixMillis" to { v -> timestampToUnixMillis(v) },
94+
"unixSecondsToTimestamp" to { v -> unixSecondsToTimestamp(v) },
95+
"timestampToUnixSeconds" to { v -> timestampToUnixSeconds(v) }
96+
)
97+
98+
val testCases =
99+
listOf(
100+
UnaryTestCase(NULL_INPUT, ExpectedOutcome.NULL, "NULL"),
101+
UnaryTestCase(ERROR_INPUT, ExpectedOutcome.ERROR, "ERROR"),
102+
// Unary ops expect resolved args, so UNSET should lead to an error during evaluation.
103+
UnaryTestCase(UNSET_INPUT, ExpectedOutcome.ERROR, "UNSET")
104+
)
105+
106+
for ((funcName, builder) in unaryFunctionBuilders) {
107+
for (testCase in testCases) {
108+
val exprToEvaluate = builder(testCase.inputExpr)
109+
val result = evaluate(exprToEvaluate) // Assumes default document context
110+
111+
when (testCase.expectedOutcome) {
112+
ExpectedOutcome.NULL ->
113+
assertEvaluatesToNull(result, "Function: %s, Input: %s", funcName, testCase.description)
114+
ExpectedOutcome.ERROR ->
115+
assertEvaluatesToError(
116+
result,
117+
"Function: %s, Input: %s",
118+
funcName,
119+
testCase.description
120+
)
121+
}
122+
}
123+
}
124+
}
125+
126+
@Test
127+
fun `binary function input mirroring`() {
128+
val binaryFunctionBuilders =
129+
listOf<Pair<String, (Expr, Expr) -> Expr>>(
130+
// Arithmetic (Variadic, base is binary)
131+
"add" to { v1, v2 -> add(v1, v2) },
132+
"subtract" to { v1, v2 -> subtract(v1, v2) },
133+
"multiply" to { v1, v2 -> multiply(v1, v2) },
134+
"divide" to { v1, v2 -> divide(v1, v2) },
135+
"mod" to { v1, v2 -> mod(v1, v2) },
136+
// Comparison
137+
"eq" to { v1, v2 -> eq(v1, v2) },
138+
"neq" to { v1, v2 -> neq(v1, v2) },
139+
"lt" to { v1, v2 -> lt(v1, v2) },
140+
"lte" to { v1, v2 -> lte(v1, v2) },
141+
"gt" to { v1, v2 -> gt(v1, v2) },
142+
"gte" to { v1, v2 -> gte(v1, v2) },
143+
// Array
144+
"arrayContains" to { v1, v2 -> arrayContains(v1, v2) },
145+
"arrayContainsAll" to { v1, v2 -> arrayContainsAll(v1, v2) },
146+
"arrayContainsAny" to { v1, v2 -> arrayContainsAny(v1, v2) },
147+
"eqAny" to { v1, v2 -> eqAny(v1, v2) }, // Maps to EqAnyExpr
148+
"notEqAny" to { v1, v2 -> notEqAny(v1, v2) }, // Maps to NotEqAnyExpr
149+
// String
150+
"like" to { v1, v2 -> like(v1, v2) },
151+
"regexContains" to { v1, v2 -> regexContains(v1, v2) },
152+
"regexMatch" to { v1, v2 -> regexMatch(v1, v2) },
153+
"strContains" to { v1, v2 -> strContains(v1, v2) }, // Maps to StrContainsExpr
154+
"startsWith" to { v1, v2 -> startsWith(v1, v2) },
155+
"endsWith" to { v1, v2 -> endsWith(v1, v2) },
156+
"strConcat" to { v1, v2 -> strConcat(v1, v2) } // Maps to StrConcatExpr
157+
// TODO(b/351084804): mapGet is not implemented yet
158+
)
159+
160+
val testCases =
161+
listOf(
162+
// Rule 1: NULL, NULL -> NULL (for most ops, some like eq(NULL,NULL) might be NULL)
163+
BinaryTestCase(NULL_INPUT, NULL_INPUT, ExpectedOutcome.NULL, "NULL, NULL -> NULL"),
164+
// Rule 2: Error/Unset propagation
165+
BinaryTestCase(NULL_INPUT, ERROR_INPUT, ExpectedOutcome.ERROR, "NULL, ERROR -> ERROR"),
166+
BinaryTestCase(ERROR_INPUT, NULL_INPUT, ExpectedOutcome.ERROR, "ERROR, NULL -> ERROR"),
167+
BinaryTestCase(NULL_INPUT, UNSET_INPUT, ExpectedOutcome.ERROR, "NULL, UNSET -> ERROR"),
168+
BinaryTestCase(UNSET_INPUT, NULL_INPUT, ExpectedOutcome.ERROR, "UNSET, NULL -> ERROR"),
169+
BinaryTestCase(ERROR_INPUT, ERROR_INPUT, ExpectedOutcome.ERROR, "ERROR, ERROR -> ERROR"),
170+
BinaryTestCase(ERROR_INPUT, UNSET_INPUT, ExpectedOutcome.ERROR, "ERROR, UNSET -> ERROR"),
171+
BinaryTestCase(UNSET_INPUT, ERROR_INPUT, ExpectedOutcome.ERROR, "UNSET, ERROR -> ERROR"),
172+
BinaryTestCase(UNSET_INPUT, UNSET_INPUT, ExpectedOutcome.ERROR, "UNSET, UNSET -> ERROR"),
173+
BinaryTestCase(VALID_INPUT, ERROR_INPUT, ExpectedOutcome.ERROR, "VALID, ERROR -> ERROR"),
174+
BinaryTestCase(ERROR_INPUT, VALID_INPUT, ExpectedOutcome.ERROR, "ERROR, VALID -> ERROR"),
175+
BinaryTestCase(VALID_INPUT, UNSET_INPUT, ExpectedOutcome.ERROR, "VALID, UNSET -> ERROR"),
176+
BinaryTestCase(UNSET_INPUT, VALID_INPUT, ExpectedOutcome.ERROR, "UNSET, VALID -> ERROR")
177+
)
178+
179+
for ((funcName, builder) in binaryFunctionBuilders) {
180+
for (testCase in testCases) {
181+
val exprToEvaluate = builder(testCase.left, testCase.right)
182+
val result = evaluate(exprToEvaluate) // Assumes default document context
183+
184+
when (testCase.expectedOutcome) {
185+
ExpectedOutcome.NULL ->
186+
assertEvaluatesToNull(result, "Function: %s, Case: %s", funcName, testCase.description)
187+
ExpectedOutcome.ERROR ->
188+
assertEvaluatesToError(result, "Function: %s, Case: %s", funcName, testCase.description)
189+
}
190+
}
191+
}
192+
}
193+
}

0 commit comments

Comments
 (0)