Skip to content

Commit 785cf1f

Browse files
committed
migrate upstream tests
1 parent 4e5e546 commit 785cf1f

File tree

2 files changed

+212
-39
lines changed

2 files changed

+212
-39
lines changed

NRedisStack.sln.DotSettings

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=geoshape/@EntryIndexedValue">True</s:Boolean>
3+
<s:Boolean x:Key="/Default/UserDictionary/Words/=hnsw/@EntryIndexedValue">True</s:Boolean>
4+
<s:Boolean x:Key="/Default/UserDictionary/Words/=indexempty/@EntryIndexedValue">True</s:Boolean>
5+
<s:Boolean x:Key="/Default/UserDictionary/Words/=indexmissing/@EntryIndexedValue">True</s:Boolean>
6+
<s:Boolean x:Key="/Default/UserDictionary/Words/=ismissing/@EntryIndexedValue">True</s:Boolean>
7+
<s:Boolean x:Key="/Default/UserDictionary/Words/=vamana/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

tests/NRedisStack.Tests/Search/IndexCreationTests.cs

Lines changed: 205 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Runtime.InteropServices;
12
using StackExchange.Redis;
23
using NRedisStack.Search;
34
using NRedisStack.RedisStackCommands;
@@ -11,31 +12,40 @@ public class IndexCreationTests(EndpointsFixture endpointsFixture)
1112
{
1213
private readonly string index = "MISSING_EMPTY_INDEX";
1314

15+
// ReSharper disable once InconsistentNaming
1416
private static readonly string INDEXMISSING = "INDEXMISSING";
17+
18+
// ReSharper disable once InconsistentNaming
1519
private static readonly string INDEXEMPTY = "INDEXEMPTY";
20+
21+
// ReSharper disable once InconsistentNaming
1622
private static readonly string SORTABLE = "SORTABLE";
1723

1824
[Fact]
1925
public void TestMissingEmptyFieldCommandArgs()
2026
{
2127
Schema sc = new Schema()
22-
.AddTextField("text1", 1.0, missingIndex: true, emptyIndex: true)
23-
.AddTagField("tag1", missingIndex: true, emptyIndex: true)
24-
.AddNumericField("numeric1", missingIndex: true)
25-
.AddGeoField("geo1", missingIndex: true)
26-
.AddGeoShapeField("geoshape1", Schema.GeoShapeField.CoordinateSystem.FLAT, missingIndex: true)
27-
.AddVectorField("vector1", Schema.VectorField.VectorAlgo.FLAT, missingIndex: true);
28+
// ReSharper disable once RedundantArgumentDefaultValue
29+
.AddTextField("text1", 1.0, missingIndex: true, emptyIndex: true)
30+
.AddTagField("tag1", missingIndex: true, emptyIndex: true)
31+
.AddNumericField("numeric1", missingIndex: true)
32+
.AddGeoField("geo1", missingIndex: true)
33+
.AddGeoShapeField("geoshape1", Schema.GeoShapeField.CoordinateSystem.FLAT, missingIndex: true)
34+
.AddVectorField("vector1", Schema.VectorField.VectorAlgo.FLAT, missingIndex: true);
2835

2936
var ftCreateParams = FTCreateParams.CreateParams();
3037

3138
var cmd = SearchCommandBuilder.Create(index, ftCreateParams, sc);
32-
var expectedArgs = new object[] { "MISSING_EMPTY_INDEX", "SCHEMA",
33-
"text1","TEXT",INDEXMISSING,INDEXEMPTY,
34-
"tag1","TAG", INDEXMISSING,INDEXEMPTY,
35-
"numeric1","NUMERIC", INDEXMISSING,
36-
"geo1","GEO", INDEXMISSING,
37-
"geoshape1","GEOSHAPE", "FLAT", INDEXMISSING,
38-
"vector1","VECTOR","FLAT", 0, INDEXMISSING};
39+
var expectedArgs = new object[]
40+
{
41+
"MISSING_EMPTY_INDEX", "SCHEMA",
42+
"text1", "TEXT", INDEXMISSING, INDEXEMPTY,
43+
"tag1", "TAG", INDEXMISSING, INDEXEMPTY,
44+
"numeric1", "NUMERIC", INDEXMISSING,
45+
"geo1", "GEO", INDEXMISSING,
46+
"geoshape1", "GEOSHAPE", "FLAT", INDEXMISSING,
47+
"vector1", "VECTOR", "FLAT", 0, INDEXMISSING
48+
};
3949
Assert.Equal(expectedArgs, cmd.Args);
4050
}
4151

@@ -44,6 +54,7 @@ public void TestMissingEmptyFieldCommandArgs()
4454
public void TestMissingFields(string endpointId)
4555
{
4656
IDatabase db = GetCleanDatabase(endpointId);
57+
// ReSharper disable once RedundantArgumentDefaultValue
4758
var ft = db.FT(2);
4859
var vectorAttrs = new Dictionary<string, object>()
4960
{
@@ -52,12 +63,13 @@ public void TestMissingFields(string endpointId)
5263
["DISTANCE_METRIC"] = "L2",
5364
};
5465
Schema sc = new Schema()
55-
.AddTextField("text1", 1.0, missingIndex: true)
56-
.AddTagField("tag1", missingIndex: true)
57-
.AddNumericField("numeric1", missingIndex: true)
58-
.AddGeoField("geo1", missingIndex: true)
59-
.AddGeoShapeField("geoshape1", Schema.GeoShapeField.CoordinateSystem.FLAT, missingIndex: true)
60-
.AddVectorField("vector1", Schema.VectorField.VectorAlgo.FLAT, vectorAttrs, missingIndex: true);
66+
// ReSharper disable once RedundantArgumentDefaultValue
67+
.AddTextField("text1", 1.0, missingIndex: true)
68+
.AddTagField("tag1", missingIndex: true)
69+
.AddNumericField("numeric1", missingIndex: true)
70+
.AddGeoField("geo1", missingIndex: true)
71+
.AddGeoShapeField("geoshape1", Schema.GeoShapeField.CoordinateSystem.FLAT, missingIndex: true)
72+
.AddVectorField("vector1", Schema.VectorField.VectorAlgo.FLAT, vectorAttrs, missingIndex: true);
6173

6274
var ftCreateParams = FTCreateParams.CreateParams();
6375
Assert.True(ft.Create(index, ftCreateParams, sc));
@@ -67,7 +79,11 @@ public void TestMissingFields(string endpointId)
6779

6880
Polygon polygon = new GeometryFactory().CreatePolygon([new(1, 1), new(10, 10), new(100, 100), new(1, 1)]);
6981

70-
var hashWithAllFields = new HashEntry[] { new("text1", "value1"), new("tag1", "value2"), new("numeric1", "3.141"), new("geo1", "-0.441,51.458"), new("geoshape1", polygon.ToString()), new("vector1", "aaaaaaaa") };
82+
var hashWithAllFields = new HashEntry[]
83+
{
84+
new("text1", "value1"), new("tag1", "value2"), new("numeric1", "3.141"), new("geo1", "-0.441,51.458"),
85+
new("geoshape1", polygon.ToString()), new("vector1", "aaaaaaaa")
86+
};
7187
db.HashSet("hashWithAllFields", hashWithAllFields);
7288

7389
var result = ft.Search(index, new("ismissing(@text1)"));
@@ -100,10 +116,12 @@ public void TestMissingFields(string endpointId)
100116
public void TestEmptyFields(string endpointId)
101117
{
102118
IDatabase db = GetCleanDatabase(endpointId);
119+
// ReSharper disable once RedundantArgumentDefaultValue
103120
var ft = db.FT(2);
104121
Schema sc = new Schema()
105-
.AddTextField("text1", 1.0, emptyIndex: true)
106-
.AddTagField("tag1", emptyIndex: true);
122+
// ReSharper disable once RedundantArgumentDefaultValue
123+
.AddTextField("text1", 1.0, emptyIndex: true)
124+
.AddTagField("tag1", emptyIndex: true);
107125

108126
var ftCreateParams = FTCreateParams.CreateParams();
109127
Assert.True(ft.Create(index, ftCreateParams, sc));
@@ -121,14 +139,14 @@ public void TestEmptyFields(string endpointId)
121139
result = ft.Search(index, new("@tag1:{''}"));
122140
Assert.Equal(1, result.TotalResults);
123141
Assert.Equal("hashWithEmptyFields", result.Documents[0].Id);
124-
125142
}
126143

127144
[SkipIfRedisTheory(Comparison.LessThan, "7.3.240")]
128145
[MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))]
129146
public void TestCreateFloat16VectorField(string endpointId)
130147
{
131148
IDatabase db = GetCleanDatabase(endpointId);
149+
// ReSharper disable once RedundantArgumentDefaultValue
132150
var ft = db.FT(2);
133151
var schema = new Schema().AddVectorField("v", Schema.VectorField.VectorAlgo.FLAT, new()
134152
{
@@ -170,6 +188,7 @@ public void TestCreateFloat16VectorField(string endpointId)
170188
public void TestCreateInt8VectorField(string endpointId)
171189
{
172190
IDatabase db = GetCleanDatabase(endpointId);
191+
// ReSharper disable once RedundantArgumentDefaultValue
173192
var ft = db.FT(2);
174193
var schema = new Schema().AddVectorField("v", Schema.VectorField.VectorAlgo.FLAT, new()
175194
{
@@ -206,19 +225,23 @@ public void TestMissingSortableFieldCommandArgs()
206225
{
207226
string idx = "MISSING_EMPTY_SORTABLE_INDEX";
208227
Schema sc = new Schema()
209-
.AddTextField("text1", 1.0, missingIndex: true, emptyIndex: true, sortable: true)
210-
.AddTagField("tag1", missingIndex: true, emptyIndex: true, sortable: true)
211-
.AddNumericField("numeric1", missingIndex: true, sortable: true)
212-
.AddGeoField("geo1", missingIndex: true, sortable: true);
228+
// ReSharper disable once RedundantArgumentDefaultValue
229+
.AddTextField("text1", 1.0, missingIndex: true, emptyIndex: true, sortable: true)
230+
.AddTagField("tag1", missingIndex: true, emptyIndex: true, sortable: true)
231+
.AddNumericField("numeric1", missingIndex: true, sortable: true)
232+
.AddGeoField("geo1", missingIndex: true, sortable: true);
213233

214234
var ftCreateParams = FTCreateParams.CreateParams();
215235

216236
var cmd = SearchCommandBuilder.Create(idx, ftCreateParams, sc);
217-
var expectedArgs = new object[] { idx, "SCHEMA",
218-
"text1","TEXT",INDEXMISSING,INDEXEMPTY,SORTABLE,
219-
"tag1","TAG", INDEXMISSING,INDEXEMPTY,SORTABLE,
220-
"numeric1","NUMERIC", INDEXMISSING,SORTABLE,
221-
"geo1","GEO", INDEXMISSING, SORTABLE};
237+
var expectedArgs = new object[]
238+
{
239+
idx, "SCHEMA",
240+
"text1", "TEXT", INDEXMISSING, INDEXEMPTY, SORTABLE,
241+
"tag1", "TAG", INDEXMISSING, INDEXEMPTY, SORTABLE,
242+
"numeric1", "NUMERIC", INDEXMISSING, SORTABLE,
243+
"geo1", "GEO", INDEXMISSING, SORTABLE
244+
};
222245
Assert.Equal(expectedArgs, cmd.Args);
223246
}
224247

@@ -228,6 +251,7 @@ public void TestCombiningMissingEmptySortableFields(string endpointId)
228251
{
229252
string idx = "MISSING_EMPTY_SORTABLE_INDEX";
230253
IDatabase db = GetCleanDatabase(endpointId);
254+
// ReSharper disable once RedundantArgumentDefaultValue
231255
var ft = db.FT(2);
232256
var vectorAttrs = new Dictionary<string, object>()
233257
{
@@ -236,12 +260,13 @@ public void TestCombiningMissingEmptySortableFields(string endpointId)
236260
["DISTANCE_METRIC"] = "L2",
237261
};
238262
Schema sc = new Schema()
239-
.AddTextField("text1", 1.0, missingIndex: true, emptyIndex: true, sortable: true)
240-
.AddTagField("tag1", missingIndex: true, emptyIndex: true, sortable: true)
241-
.AddNumericField("numeric1", missingIndex: true, sortable: true)
242-
.AddGeoField("geo1", missingIndex: true, sortable: true)
243-
.AddGeoShapeField("geoshape1", Schema.GeoShapeField.CoordinateSystem.FLAT, missingIndex: true)
244-
.AddVectorField("vector1", Schema.VectorField.VectorAlgo.FLAT, vectorAttrs, missingIndex: true);
263+
// ReSharper disable once RedundantArgumentDefaultValue
264+
.AddTextField("text1", 1.0, missingIndex: true, emptyIndex: true, sortable: true)
265+
.AddTagField("tag1", missingIndex: true, emptyIndex: true, sortable: true)
266+
.AddNumericField("numeric1", missingIndex: true, sortable: true)
267+
.AddGeoField("geo1", missingIndex: true, sortable: true)
268+
.AddGeoShapeField("geoshape1", Schema.GeoShapeField.CoordinateSystem.FLAT, missingIndex: true)
269+
.AddVectorField("vector1", Schema.VectorField.VectorAlgo.FLAT, vectorAttrs, missingIndex: true);
245270

246271
var ftCreateParams = FTCreateParams.CreateParams();
247272
Assert.True(ft.Create(idx, ftCreateParams, sc));
@@ -251,7 +276,11 @@ public void TestCombiningMissingEmptySortableFields(string endpointId)
251276

252277
Polygon polygon = new GeometryFactory().CreatePolygon([new(1, 1), new(10, 10), new(100, 100), new(1, 1)]);
253278

254-
var hashWithAllFields = new HashEntry[] { new("text1", "value1"), new("tag1", "value2"), new("numeric1", "3.141"), new("geo1", "-0.441,51.458"), new("geoshape1", polygon.ToString()), new("vector1", "aaaaaaaa") };
279+
var hashWithAllFields = new HashEntry[]
280+
{
281+
new("text1", "value1"), new("tag1", "value2"), new("numeric1", "3.141"), new("geo1", "-0.441,51.458"),
282+
new("geoshape1", polygon.ToString()), new("vector1", "aaaaaaaa")
283+
};
255284
db.HashSet("hashWithAllFields", hashWithAllFields);
256285

257286
var result = ft.Search(idx, new("ismissing(@text1)"));
@@ -279,5 +308,142 @@ public void TestCombiningMissingEmptySortableFields(string endpointId)
279308
Assert.Equal("hashWithMissingFields", result.Documents[0].Id);
280309
}
281310

311+
[SkipIfRedisTheory(Comparison.LessThan, "8.1.240")]
312+
[MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))]
313+
public void TestCreate_Float16_Int32_VectorField_Svs(string endpointId)
314+
{
315+
IDatabase db = GetCleanDatabase(endpointId);
316+
// ReSharper disable once RedundantArgumentDefaultValue
317+
var ft = db.FT(2);
318+
var schema = new Schema().AddSvsVanamaVectorField("v", Schema.VectorField.VectorType.FLOAT16, 5,
319+
Schema.VectorField.VectorDistanceMetric.EuclideanDistance)
320+
.AddSvsVanamaVectorField("v2", Schema.VectorField.VectorType.FLOAT32, 4,
321+
Schema.VectorField.VectorDistanceMetric.EuclideanDistance);
322+
323+
var cmd = SearchCommandBuilder.Create("idx", FTCreateParams.CreateParams(), schema).ToString();
324+
Log(cmd);
325+
326+
Assert.True(ft.Create("idx", new FTCreateParams(), schema));
327+
328+
byte[] vec1ToBytes = MemoryMarshal.AsBytes(stackalloc short[] { 2, 1, 2, 2, 2 }).ToArray();
329+
byte[] vec2ToBytes = MemoryMarshal.AsBytes(stackalloc int[] { 1, 2, 2, 2 }).ToArray();
330+
331+
HashEntry[] entries = [new("v", vec1ToBytes), new("v2", vec2ToBytes)];
332+
db.HashSet("a", entries);
333+
db.HashSet("b", entries);
334+
db.HashSet("c", entries);
335+
336+
var q = new Query("*=>[KNN 2 @v $vec]").ReturnFields("__v_score");
337+
var res = ft.Search("idx", q.AddParam("vec", vec1ToBytes));
338+
Assert.Equal(2, res.TotalResults);
339+
340+
q = new Query("*=>[KNN 2 @v2 $vec]").ReturnFields("__v_score");
341+
res = ft.Search("idx", q.AddParam("vec", vec2ToBytes));
342+
Assert.Equal(2, res.TotalResults);
343+
}
344+
345+
[SkipIfRedisTheory(Comparison.LessThan, "8.1.240")]
346+
[MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))]
347+
public void TestCreate_Float16_Int32_VectorField_Svs_WithCompression(string endpointId)
348+
{
349+
IDatabase db = GetCleanDatabase(endpointId);
350+
// ReSharper disable once RedundantArgumentDefaultValue
351+
var ft = db.FT(2);
352+
var schema = new Schema().AddSvsVanamaVectorField("v", Schema.VectorField.VectorType.FLOAT16, 5,
353+
Schema.VectorField.VectorDistanceMetric.EuclideanDistance,
354+
reducedDimensions: 2, compressionAlgorithm: Schema.VectorField.VectorCompressionAlgorithm.LeanVec4x8)
355+
.AddSvsVanamaVectorField("v2", Schema.VectorField.VectorType.FLOAT32, 4,
356+
Schema.VectorField.VectorDistanceMetric.EuclideanDistance,
357+
compressionAlgorithm: Schema.VectorField.VectorCompressionAlgorithm.LVQ4);
358+
359+
var cmd = SearchCommandBuilder.Create("idx", FTCreateParams.CreateParams(), schema).ToString();
360+
Log(cmd);
361+
362+
Assert.True(ft.Create("idx", new FTCreateParams(), schema));
282363

364+
byte[] vec1ToBytes = MemoryMarshal.AsBytes(stackalloc short[] { 2, 1, 2, 2, 2 }).ToArray();
365+
byte[] vec2ToBytes = MemoryMarshal.AsBytes(stackalloc int[] { 1, 2, 2, 2 }).ToArray();
366+
367+
HashEntry[] entries = [new("v", vec1ToBytes), new("v2", vec2ToBytes)];
368+
db.HashSet("a", entries);
369+
db.HashSet("b", entries);
370+
db.HashSet("c", entries);
371+
372+
var q = new Query("*=>[KNN 2 @v $vec]").ReturnFields("__v_score");
373+
var res = ft.Search("idx", q.AddParam("vec", vec1ToBytes));
374+
Assert.Equal(2, res.TotalResults);
375+
376+
q = new Query("*=>[KNN 2 @v2 $vec]").ReturnFields("__v_score");
377+
res = ft.Search("idx", q.AddParam("vec", vec2ToBytes));
378+
Assert.Equal(2, res.TotalResults);
379+
}
380+
381+
[Fact]
382+
public void TestIndexingCreation_Default()
383+
{
384+
Schema sc = new Schema()
385+
.AddFlatVectorField("vector1", Schema.VectorField.VectorType.FLOAT32, 2,
386+
Schema.VectorField.VectorDistanceMetric.EuclideanDistance, missingIndex: true)
387+
.AddHnswVectorField("vector2", Schema.VectorField.VectorType.FLOAT64, 3,
388+
Schema.VectorField.VectorDistanceMetric.CosineDistance, missingIndex: false)
389+
.AddSvsVanamaVectorField("vector3", Schema.VectorField.VectorType.FLOAT16, 4,
390+
Schema.VectorField.VectorDistanceMetric.InnerProduct, missingIndex: true);
391+
392+
var ftCreateParams = FTCreateParams.CreateParams();
393+
var cmd = SearchCommandBuilder.Create("IDX_NAME", ftCreateParams, sc).ToString();
394+
395+
Assert.Equal(
396+
"FT.CREATE IDX_NAME SCHEMA vector1 VECTOR FLAT 6 DIM 2 TYPE FLOAT32 DISTANCE_METRIC L2 INDEXMISSING vector2 VECTOR HNSW 6 DIM 3 TYPE FLOAT64 DISTANCE_METRIC COSINE vector3 VECTOR SVS-VAMANA 6 DIM 4 TYPE FLOAT16 DISTANCE_METRIC IP INDEXMISSING",
397+
cmd);
398+
}
399+
400+
[Fact]
401+
public void TestIndexingCreation_WithAttribs()
402+
{
403+
Schema sc = new Schema()
404+
.AddFlatVectorField("vector1", Schema.VectorField.VectorType.NotSpecified, 0,
405+
Schema.VectorField.VectorDistanceMetric.NotSpecified, missingIndex: true,
406+
attributes: new Dictionary<string, object>()
407+
{
408+
["TYPE"] = "FUT1", // some values not representable in the old API
409+
["DIM"] = "FUT2",
410+
["DISTANCE_METRIC"] = "FUT3",
411+
["NEW_FIELD"] = "NEW_VALUE",
412+
});
413+
414+
var ftCreateParams = FTCreateParams.CreateParams();
415+
var cmd = SearchCommandBuilder.Create("IDX_NAME", ftCreateParams, sc).ToString();
416+
417+
Assert.Equal(
418+
"FT.CREATE IDX_NAME SCHEMA vector1 VECTOR FLAT 8 TYPE FUT1 DIM FUT2 DISTANCE_METRIC FUT3 NEW_FIELD NEW_VALUE INDEXMISSING",
419+
cmd);
420+
}
421+
422+
[Fact]
423+
public void TestIndexingCreation_Custom_Everything()
424+
{
425+
Schema sc = new Schema()
426+
.AddFlatVectorField("vector1", Schema.VectorField.VectorType.FLOAT32, 2,
427+
Schema.VectorField.VectorDistanceMetric.EuclideanDistance, missingIndex: true)
428+
.AddHnswVectorField("vector2", Schema.VectorField.VectorType.FLOAT64, 3,
429+
Schema.VectorField.VectorDistanceMetric.CosineDistance,
430+
maxOutgoingConnections: 10, maxConnectedNeighbors: 20, maxTopCandidates: 30, boundaryFactor: 0.7,
431+
missingIndex: false)
432+
.AddSvsVanamaVectorField("vector3", Schema.VectorField.VectorType.FLOAT16, 4,
433+
Schema.VectorField.VectorDistanceMetric.InnerProduct,
434+
compressionAlgorithm: Schema.VectorField.VectorCompressionAlgorithm.LeanVec4x8,
435+
constructionWindowSize: 35, graphMaxDegree: 17, searchWindowSize: 30,
436+
rangeSearchApproximationFactor: 0.5,
437+
trainingThreshold: 100, reducedDimensions: 50,
438+
missingIndex: true);
439+
440+
var ftCreateParams = FTCreateParams.CreateParams();
441+
var cmd = SearchCommandBuilder.Create("IDX_NAME", ftCreateParams, sc).ToString();
442+
443+
Assert.Equal(
444+
"FT.CREATE IDX_NAME SCHEMA vector1 VECTOR FLAT 6 DIM 2 TYPE FLOAT32 DISTANCE_METRIC L2 INDEXMISSING " +
445+
"vector2 VECTOR HNSW 14 DIM 3 TYPE FLOAT64 DISTANCE_METRIC COSINE M 10 EF_CONSTRUCTION 20 EF_RUNTIME 30 EPSILON 0.7 " +
446+
"vector3 VECTOR SVS-VAMANA 20 COMPRESSION LeanVec4x8 DIM 4 TYPE FLOAT16 DISTANCE_METRIC IP CONSTRUCTION_WINDOW_SIZE 35 GRAPH_MAX_DEGREE 17 SEARCH_WINDOW_SIZE 30 EPSILON 0.5 TRAINING_THRESHOLD 100 REDUCE 50 INDEXMISSING",
447+
cmd);
448+
}
283449
}

0 commit comments

Comments
 (0)