OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package memory | 5 package memory |
6 | 6 |
7 import ( | 7 import ( |
8 "bytes" | 8 "bytes" |
9 "math" | |
10 "testing" | 9 "testing" |
11 | 10 |
12 » dsS "github.com/luci/gae/service/datastore" | 11 » dstore "github.com/luci/gae/service/datastore" |
13 "github.com/luci/gae/service/datastore/serialize" | 12 "github.com/luci/gae/service/datastore/serialize" |
14 "github.com/luci/luci-go/common/cmpbin" | 13 "github.com/luci/luci-go/common/cmpbin" |
| 14 "github.com/luci/luci-go/common/stringset" |
15 . "github.com/luci/luci-go/common/testing/assertions" | 15 . "github.com/luci/luci-go/common/testing/assertions" |
16 . "github.com/smartystreets/goconvey/convey" | 16 . "github.com/smartystreets/goconvey/convey" |
17 "golang.org/x/net/context" | |
18 ) | 17 ) |
19 | 18 |
20 const ( | |
21 MaxUint = ^uint(0) | |
22 MaxInt = int(MaxUint >> 1) | |
23 IntIs32Bits = int64(MaxInt) < math.MaxInt64 | |
24 ) | |
25 | |
26 func TestDatastoreQueries(t *testing.T) { | |
27 Convey("Datastore Query suport", t, func() { | |
28 c := Use(context.Background()) | |
29 ds := dsS.Get(c) | |
30 So(ds, ShouldNotBeNil) | |
31 | |
32 Convey("can create good queries", func() { | |
33 q := ds.NewQuery("Foo").Filter("farnsworth >", 20).KeysO
nly().Limit(10).Offset(39) | |
34 | |
35 // normally you can only get cursors from inside of the
memory | |
36 // implementation, so this construction is just for test
ing. | |
37 start := queryCursor(bjoin( | |
38 mkNum(2), | |
39 serialize.ToBytes(dsS.IndexColumn{Property: "far
nsworth"}), | |
40 serialize.ToBytes(dsS.IndexColumn{Property: "__k
ey__"}), | |
41 serialize.ToBytes(prop(200)), | |
42 serialize.ToBytes(prop(ds.NewKey("Foo", "id", 0,
nil))))) | |
43 | |
44 So(start.String(), ShouldEqual, | |
45 `gYAAZzFdTeeb3d9zOxsAAF-v221Xy32_AIGHyIgAAUc32-A
FabMAAA==`) | |
46 | |
47 end := queryCursor(bjoin( | |
48 mkNum(2), | |
49 serialize.ToBytes(dsS.IndexColumn{Property: "far
nsworth"}), | |
50 serialize.ToBytes(dsS.IndexColumn{Property: "__k
ey__"}), | |
51 serialize.ToBytes(prop(3000)), | |
52 serialize.ToBytes(prop(ds.NewKey("Foo", "zeta",
0, nil))))) | |
53 | |
54 q = q.Start(start).End(end) | |
55 So(q, ShouldNotBeNil) | |
56 So(q.(*queryImpl).err, ShouldBeNil) | |
57 rq, err := q.(*queryImpl).reduce("", false) | |
58 So(rq, ShouldNotBeNil) | |
59 So(err, ShouldBeNil) | |
60 }) | |
61 | |
62 Convey("ensures orders make sense", func() { | |
63 q := ds.NewQuery("Cool") | |
64 q = q.Filter("cat =", 19).Filter("bob =", 10).Order("bob
").Order("bob") | |
65 | |
66 Convey("removes dups and equality orders", func() { | |
67 q = q.Order("wat") | |
68 qi := q.(*queryImpl) | |
69 So(qi.err, ShouldBeNil) | |
70 rq, err := qi.reduce("", false) | |
71 So(err, ShouldBeNil) | |
72 So(rq.suffixFormat, ShouldResemble, []dsS.IndexC
olumn{ | |
73 {Property: "wat"}, {Property: "__key__"}
}) | |
74 }) | |
75 | |
76 Convey("if we equality-filter on __key__, that's just si
lly", func() { | |
77 q = q.Order("wat").Filter("__key__ =", ds.NewKey
("Foo", "wat", 0, nil)) | |
78 _, err := q.(*queryImpl).reduce("", false) | |
79 So(err, ShouldErrLike, "query equality filter on
__key__ is silly") | |
80 }) | |
81 | |
82 }) | |
83 | |
84 }) | |
85 } | |
86 | |
87 type queryTest struct { | |
88 // name is the name of the test case | |
89 name string | |
90 | |
91 // q is the input query | |
92 q dsS.Query | |
93 | |
94 // err is the error to expect after prepping the query (error, string or
nil) | |
95 err interface{} | |
96 | |
97 // equivalentQuery is another query which ShouldResemble q. This is usef
ul to | |
98 // see the effects of redundancy pruning on e.g. filters. | |
99 equivalentQuery dsS.Query | |
100 } | |
101 | |
102 type sillyCursor string | 19 type sillyCursor string |
103 | 20 |
104 func (s sillyCursor) String() string { return string(s) } | 21 func (s sillyCursor) String() string { return string(s) } |
105 | 22 |
106 func curs(pairs ...interface{}) queryCursor { | 23 func curs(pairs ...interface{}) queryCursor { |
107 if len(pairs)%2 != 0 { | 24 if len(pairs)%2 != 0 { |
108 panic("curs() takes only even pairs") | 25 panic("curs() takes only even pairs") |
109 } | 26 } |
110 pre := &bytes.Buffer{} | 27 pre := &bytes.Buffer{} |
111 » cmpbin.WriteUint(pre, uint64(len(pairs)/2)) | 28 » if _, err := cmpbin.WriteUint(pre, uint64(len(pairs)/2)); err != nil { |
| 29 » » panic(err) |
| 30 » } |
112 post := serialize.Invertible(&bytes.Buffer{}) | 31 post := serialize.Invertible(&bytes.Buffer{}) |
113 for i := 0; i < len(pairs); i += 2 { | 32 for i := 0; i < len(pairs); i += 2 { |
114 k, v := pairs[i].(string), pairs[i+1] | 33 k, v := pairs[i].(string), pairs[i+1] |
115 | 34 |
116 » » col := dsS.IndexColumn{Property: k} | 35 » » col, err := dstore.ParseIndexColumn(k) |
| 36 » » if err != nil { |
| 37 » » » panic(err) |
| 38 » » } |
117 | 39 |
118 » » post.SetInvert(false) | 40 » » post.SetInvert(col.Descending) |
119 » » if k[0] == '-' { | 41 » » if err := serialize.WriteIndexColumn(pre, col); err != nil { |
120 » » » post.SetInvert(false) | 42 » » » panic(err) |
121 » » » col.Property = k[1:] | |
122 » » » col.Direction = dsS.DESCENDING | |
123 } | 43 } |
124 » » serialize.WriteIndexColumn(pre, col) | 44 » » if err := serialize.WriteProperty(post, serialize.WithoutContext
, prop(v)); err != nil { |
125 » » serialize.WriteProperty(post, serialize.WithoutContext, prop(v)) | 45 » » » panic(err) |
| 46 » » } |
126 } | 47 } |
127 return queryCursor(bjoin(pre.Bytes(), post.Bytes())) | 48 return queryCursor(bjoin(pre.Bytes(), post.Bytes())) |
128 } | 49 } |
129 | 50 |
| 51 type queryTest struct { |
| 52 // name is the name of the test case |
| 53 name string |
| 54 |
| 55 // q is the input query |
| 56 q *dstore.Query |
| 57 |
| 58 // err is the error to expect after prepping the query (error, string or
nil) |
| 59 err interface{} |
| 60 |
| 61 // equivalentQuery is another query which ShouldResemble q. This is usef
ul to |
| 62 // see the effects of redundancy pruning on e.g. filters. |
| 63 equivalentQuery *reducedQuery |
| 64 } |
| 65 |
130 var queryTests = []queryTest{ | 66 var queryTests = []queryTest{ |
131 {"only one inequality", | |
132 nq().Order("bob").Order("wat").Filter("bob >", 10).Filter("wat <
", 29), | |
133 "inequality filters on multiple properties", nil}, | |
134 | |
135 {"bad filter ops", | |
136 nq().Filter("Bob !", "value"), | |
137 "invalid operator \"!\"", nil}, | |
138 | |
139 {"bad filter", | |
140 nq().Filter("Bob", "value"), | |
141 "invalid filter", nil}, | |
142 | |
143 {"bad order", | |
144 nq().Order("+Bob"), | |
145 "invalid order", nil}, | |
146 | |
147 {"empty order", | |
148 nq().Order(""), | |
149 "empty order", nil}, | |
150 | |
151 {"underflow offset", | |
152 nq().Offset(-20), | |
153 "negative query offset", nil}, | |
154 | |
155 {"bad cursors (empty)", | 67 {"bad cursors (empty)", |
156 nq().Start(queryCursor("")), | 68 nq().Start(queryCursor("")), |
157 "invalid cursor", nil}, | 69 "invalid cursor", nil}, |
158 | 70 |
159 {"bad cursors (nil)", | 71 {"bad cursors (nil)", |
160 nq().Start(queryCursor("")), | 72 nq().Start(queryCursor("")), |
161 "invalid cursor", nil}, | 73 "invalid cursor", nil}, |
162 | 74 |
163 {"bad cursors (no key)", | 75 {"bad cursors (no key)", |
164 nq().End(curs("Foo", 100)), | 76 nq().End(curs("Foo", 100)), |
165 "invalid cursor", nil}, | 77 "invalid cursor", nil}, |
166 | 78 |
167 // TODO(riannucci): exclude cursors which are out-of-bounds with inequal
ity? | 79 // TODO(riannucci): exclude cursors which are out-of-bounds with inequal
ity? |
168 // I think right now you could have a query for > 10 with a start cursor
of 1. | 80 // I think right now you could have a query for > 10 with a start cursor
of 1. |
169 {"bad cursors (doesn't include ineq)", | 81 {"bad cursors (doesn't include ineq)", |
170 » » nq().Filter("Bob >", 10).Start( | 82 » » nq().Gt("Bob", 10).Start( |
171 curs("Foo", 100, "__key__", key("something", 1)), | 83 curs("Foo", 100, "__key__", key("something", 1)), |
172 ), | 84 ), |
173 "start cursor is invalid", nil}, | 85 "start cursor is invalid", nil}, |
174 | 86 |
175 {"bad cursors (doesn't include all orders)", | 87 {"bad cursors (doesn't include all orders)", |
176 nq().Order("Luci").Order("Charliene").Start( | 88 nq().Order("Luci").Order("Charliene").Start( |
177 curs("Luci", 100, "__key__", key("something", 1)), | 89 curs("Luci", 100, "__key__", key("something", 1)), |
178 ), | 90 ), |
179 "start cursor is invalid", nil}, | 91 "start cursor is invalid", nil}, |
180 | 92 |
181 {"cursor set multiple times", | |
182 nq().Order("Luci").End( | |
183 curs("Luci", 100, "__key__", key("something", 1)), | |
184 ).End( | |
185 curs("Luci", 100, "__key__", key("something", 1)), | |
186 ), | |
187 "multiply defined", nil}, | |
188 | |
189 {"cursor bad type", | 93 {"cursor bad type", |
190 nq().Order("Luci").End(sillyCursor("I am a banana")), | 94 nq().Order("Luci").End(sillyCursor("I am a banana")), |
191 » » "unknown type", nil}, | 95 » » "bad cursor type", nil}, |
192 | |
193 » {"projecting a keys-only query", | |
194 » » nq().Project("hello").KeysOnly(), | |
195 » » "cannot project a keysOnly query", nil}, | |
196 | |
197 » {"projecting a keys-only query (reverse)", | |
198 » » nq().KeysOnly().Project("hello"), | |
199 » » "cannot project a keysOnly query", nil}, | |
200 | |
201 » {"projecting an empty field", | |
202 » » nq().Project("hello", ""), | |
203 » » "cannot project on an empty field", nil}, | |
204 | |
205 » {"projecting __key__", | |
206 » » nq().Project("hello", "__key__"), | |
207 » » "cannot project on __key__", nil}, | |
208 | |
209 » {"projecting a duplicate", | |
210 » » nq().Project("hello", "hello"), | |
211 » » "cannot project on the same field twice", nil}, | |
212 | |
213 » {"projecting a duplicate (style 2)", | |
214 » » nq().Project("hello").Project("hello"), | |
215 » » "cannot project on the same field twice", nil}, | |
216 | |
217 » {"bad ancestors", | |
218 » » nq().Ancestor(key("goop", nil)), | |
219 » » dsS.ErrInvalidKey, nil}, | |
220 | |
221 » {"nil ancestors", | |
222 » » nq().Ancestor(nil), | |
223 » » "nil query ancestor", nil}, | |
224 | |
225 » {"Bad key filters", | |
226 » » nq().Filter("__key__ >", key("goop", nil)), | |
227 » » dsS.ErrInvalidKey, nil}, | |
228 | |
229 » {"filters for __key__ that aren't keys", | |
230 » » nq().Filter("__key__ >", 10), | |
231 » » "is not a key", nil}, | |
232 | |
233 » {"multiple inequalities", | |
234 » » nq().Filter("bob > ", 19).Filter("charlie < ", 20), | |
235 » » "inequality filters on multiple properties", nil}, | |
236 | |
237 » {"inequality must be first sort order", | |
238 » » nq().Filter("bob > ", 19).Order("-charlie"), | |
239 » » "first sort order", nil}, | |
240 | |
241 » {"inequality must be first sort order (reverse)", | |
242 » » nq().Order("-charlie").Filter("bob > ", 19), | |
243 » » "first sort order", nil}, | |
244 | |
245 » {"equality filter projected field", | |
246 » » nq().Project("foo").Filter("foo = ", 10), | |
247 » » "cannot project", nil}, | |
248 | |
249 » {"equality filter projected field (reverse)", | |
250 » » nq().Filter("foo = ", 10).Project("foo"), | |
251 » » "cannot project", nil}, | |
252 | |
253 » {"kindless with non-__key__ filters", | |
254 » » nq("").Filter("face <", 25.3), | |
255 » » "kindless queries can only filter on __key__", nil}, | |
256 | |
257 » {"kindless with non-__key__ orders", | |
258 » » nq("").Order("face"), | |
259 » » "invalid order for kindless query", nil}, | |
260 | |
261 » {"kindless with descending-__key__ order", | |
262 » » nq("").Order("-__key__"), | |
263 » » "invalid order for kindless query", nil}, | |
264 | |
265 » {"bad namespace", | |
266 » » nq("something", "sup").Order("__key__"), | |
267 » » "Namespace mismatched", nil}, | |
268 | |
269 » {"distinct non-projection", | |
270 » » nq().Distinct().Filter("marla >", 1), | |
271 » » "only makes sense on projection queries", nil}, | |
272 | |
273 » {"chained errors return the first", | |
274 » » nq().Ancestor(nil).Filter("hello", "wurld").Order(""), | |
275 » » "nil query ancestor", nil}, | |
276 | |
277 » {"bad ancestor namespace", | |
278 » » nq("", "nerd").Ancestor(key("something", "correct")), | |
279 » » "bad namespace", nil}, | |
280 | |
281 » {"multiple ancestors", | |
282 » » nq().Ancestor(key("something", "correct")).Ancestor(key("somethi
ng", "else")), | |
283 » » "more than one ancestor", nil}, | |
284 | |
285 » {"filter with illegal type", | |
286 » » nq().Filter("something =", complex(1, 2)), | |
287 » » "bad type complex", nil}, | |
288 | |
289 » {"sort orders used for equality are ignored", | |
290 » » nq().Order("a").Order("b").Order("c").Filter("b =", 2), | |
291 » » nil, | |
292 » » nq().Order("a").Order("c").Filter("b =", 2)}, | |
293 | |
294 » {"sort orders used for equality are ignored (reversed)", | |
295 » » nq().Filter("b =", 2).Order("a").Order("b").Order("c"), | |
296 » » nil, | |
297 » » nq().Order("a").Order("c").Filter("b =", 2)}, | |
298 | |
299 » {"duplicate orders are ignored", | |
300 » » nq().Order("a").Order("a").Order("a"), | |
301 » » nil, | |
302 » » nq().Order("a")}, | |
303 | 96 |
304 {"overconstrained inequality (>= v <)", | 97 {"overconstrained inequality (>= v <)", |
305 » » nq().Filter("bob >=", 10).Filter("bob <", 10), | 98 » » nq().Gte("bob", 10).Lt("bob", 10), |
306 » » "done", nil}, | 99 » » dstore.ErrNullQuery, nil}, |
307 | 100 |
308 {"overconstrained inequality (> v <)", | 101 {"overconstrained inequality (> v <)", |
309 » » nq().Filter("bob >", 10).Filter("bob <", 10), | 102 » » nq().Gt("bob", 10).Lt("bob", 10), |
310 » » "done", nil}, | 103 » » dstore.ErrNullQuery, nil}, |
311 | 104 |
312 {"overconstrained inequality (> v <=)", | 105 {"overconstrained inequality (> v <=)", |
313 » » nq().Filter("bob >", 10).Filter("bob <=", 10), | 106 » » nq().Gt("bob", 10).Lte("bob", 10), |
314 » » "done", nil}, | 107 » » dstore.ErrNullQuery, nil}, |
315 | 108 |
316 {"silly inequality (=> v <=)", | 109 {"silly inequality (=> v <=)", |
317 » » nq().Filter("bob >=", 10).Filter("bob <=", 10), | 110 » » nq().Gte("bob", 10).Lte("bob", 10), |
318 » » nil, | 111 » » nil, nil}, |
319 » » nil}, | |
320 | |
321 » {"Filtering on a reserved property is forbidden", | |
322 » » nq().Filter("__special__ >=", 10), | |
323 » » "filter on reserved property", | |
324 » » nil}, | |
325 | |
326 » {"oob key filters with ancestor (highside)", | |
327 » » nq().Ancestor(key("Hello", 10)).Filter("__key__ <", key("Hello",
9)), | |
328 » » "__key__ inequality", | |
329 » » nil}, | |
330 | |
331 » {"oob key filters with ancestor (lowside)", | |
332 » » nq().Ancestor(key("Hello", 10)).Filter("__key__ >", key("Hello",
11)), | |
333 » » "__key__ inequality", | |
334 » » nil}, | |
335 | |
336 » {"in-bound key filters with ancestor OK", | |
337 » » nq().Ancestor(key("Hello", 10)).Filter("__key__ <", key("Somethi
ng", "hi", key("Hello", 10))), | |
338 » » nil, | |
339 » » nil}, | |
340 | |
341 » {"projection elements get filled in", | |
342 » » nq().Project("Foo", "Bar").Order("-Bar"), | |
343 » » nil, | |
344 » » nq().Project("Foo", "Bar").Order("-Bar").Order("Foo")}, | |
345 | 112 |
346 {"cursors get smooshed into the inquality range", | 113 {"cursors get smooshed into the inquality range", |
347 » » (nq().Filter("Foo >", 3).Filter("Foo <", 10). | 114 » » (nq().Gt("Foo", 3).Lt("Foo", 10). |
348 Start(curs("Foo", 2, "__key__", key("Something", 1))). | 115 Start(curs("Foo", 2, "__key__", key("Something", 1))). |
349 End(curs("Foo", 20, "__key__", key("Something", 20)))), | 116 End(curs("Foo", 20, "__key__", key("Something", 20)))), |
350 nil, | 117 nil, |
351 » » nq().Filter("Foo >", 3).Filter("Foo <", 10)}, | 118 » » &reducedQuery{ |
| 119 » » » "ns", "Foo", map[string]stringset.Set{}, []dstore.IndexC
olumn{ |
| 120 » » » » {Property: "Foo"}, |
| 121 » » » » {Property: "__key__"}, |
| 122 » » » }, |
| 123 » » » increment(serialize.ToBytes(dstore.MkProperty(3))), |
| 124 » » » serialize.ToBytes(dstore.MkProperty(10)), |
| 125 » » » 2, |
| 126 » » }}, |
352 | 127 |
353 {"cursors could cause the whole query to be useless", | 128 {"cursors could cause the whole query to be useless", |
354 » » (nq().Filter("Foo >", 3).Filter("Foo <", 10). | 129 » » (nq().Gt("Foo", 3).Lt("Foo", 10). |
355 Start(curs("Foo", 200, "__key__", key("Something", 1))). | 130 Start(curs("Foo", 200, "__key__", key("Something", 1))). |
356 End(curs("Foo", 1, "__key__", key("Something", 20)))), | 131 End(curs("Foo", 1, "__key__", key("Something", 20)))), |
357 » » errQueryDone, | 132 » » dstore.ErrNullQuery, |
358 nil}, | 133 nil}, |
359 | |
360 {"query without anything is fine", | |
361 nq(), | |
362 nil, | |
363 nil}, | |
364 } | |
365 | |
366 func init() { | |
367 // this is supremely stupid. The SDK uses 'int' which measn we have to | |
368 // use it too, but then THEY BOUNDS CHECK IT FOR 32 BITS... *sigh* | |
369 if !IntIs32Bits { | |
370 queryTests = append(queryTests, []queryTest{ | |
371 {"OOB limit (32 bit)", | |
372 nq().Limit(MaxInt), | |
373 "query limit overflow", nil}, | |
374 | |
375 {"OOB offset (32 bit)", | |
376 nq().Offset(MaxInt), | |
377 "query offset overflow", nil}, | |
378 }...) | |
379 } | |
380 } | 134 } |
381 | 135 |
382 func TestQueries(t *testing.T) { | 136 func TestQueries(t *testing.T) { |
383 t.Parallel() | 137 t.Parallel() |
384 | 138 |
385 Convey("queries have tons of condition checking", t, func() { | 139 Convey("queries have tons of condition checking", t, func() { |
386 for _, tc := range queryTests { | |
387 Convey(tc.name, func() { | |
388 rq, err := tc.q.(*queryImpl).reduce("ns", false) | |
389 So(err, ShouldErrLike, tc.err) | |
390 | |
391 if tc.equivalentQuery != nil { | |
392 rq2, err := tc.equivalentQuery.(*queryIm
pl).reduce("ns", false) | |
393 So(err, ShouldBeNil) | |
394 So(rq, ShouldResemble, rq2) | |
395 } | |
396 }) | |
397 } | |
398 | |
399 Convey("non-ancestor queries in a transaction", func() { | 140 Convey("non-ancestor queries in a transaction", func() { |
400 » » » _, err := nq().(*queryImpl).reduce("ns", true) | 141 » » » fq, err := nq().Finalize() |
401 » » » So(err, ShouldErrLike, "Only ancestor queries") | 142 » » » So(err, ShouldErrLike, nil) |
| 143 » » » _, err = reduce(fq, "ns", true) |
| 144 » » » So(err, ShouldErrLike, "must include an Ancestor") |
402 }) | 145 }) |
403 | 146 |
404 Convey("absurd numbers of filters are prohibited", func() { | 147 Convey("absurd numbers of filters are prohibited", func() { |
405 q := nq().Ancestor(key("thing", "wat")) | 148 q := nq().Ancestor(key("thing", "wat")) |
406 for i := 0; i < 100; i++ { | 149 for i := 0; i < 100; i++ { |
407 » » » » q = q.Filter("something =", i) | 150 » » » » q = q.Eq("something", i) |
408 } | 151 } |
409 » » » //So(q.(*queryImpl).numComponents(), ShouldEqual, 101) | 152 » » » fq, err := q.Finalize() |
410 » » » _, err := q.(*queryImpl).reduce("ns", false) | 153 » » » So(err, ShouldErrLike, nil) |
| 154 » » » _, err = reduce(fq, "ns", false) |
411 So(err, ShouldErrLike, "query is too large") | 155 So(err, ShouldErrLike, "query is too large") |
412 }) | 156 }) |
| 157 |
| 158 Convey("bulk check", func() { |
| 159 for _, tc := range queryTests { |
| 160 Convey(tc.name, func() { |
| 161 rq := (*reducedQuery)(nil) |
| 162 fq, err := tc.q.Finalize() |
| 163 if err == nil { |
| 164 err = fq.Valid("s~aid", "ns") |
| 165 if err == nil { |
| 166 rq, err = reduce(fq, "ns
", false) |
| 167 } |
| 168 } |
| 169 So(err, ShouldErrLike, tc.err) |
| 170 |
| 171 if tc.equivalentQuery != nil { |
| 172 So(rq, ShouldResemble, tc.equiva
lentQuery) |
| 173 } |
| 174 }) |
| 175 } |
| 176 }) |
413 }) | 177 }) |
414 } | 178 } |
OLD | NEW |