Skip to content

Commit

Permalink
feat: new reader tests
Browse files Browse the repository at this point in the history
  • Loading branch information
wjones127 committed Dec 4, 2022
1 parent 0277888 commit de68fc8
Show file tree
Hide file tree
Showing 64 changed files with 263 additions and 45 deletions.
131 changes: 130 additions & 1 deletion dat/generated_tables.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from decimal import Decimal
import os
from datetime import date
from datetime import date, datetime, timedelta
from pathlib import Path
import random
from typing import Callable, List, Tuple

from delta.tables import DeltaTable
import pyspark.sql
from pyspark.sql import SparkSession
import pyspark.sql.types as types

from dat.models import TableVersionMetadata, TestCaseInfo
from dat.spark_builder import get_spark_session
Expand Down Expand Up @@ -152,6 +157,23 @@ def create_multi_partitioned(case: TestCaseInfo, spark: SparkSession):
save_expected(case)


@reference_table(
name="multi_partitioned_2",
description="Multiple levels of partitioning, with boolean, timestamp, and decimal partition columns"
)
def create_multi_partitioned_2(case: TestCaseInfo, spark: SparkSession):
columns = ['bool', 'time', 'amount', 'int']
partition_columns = ['bool', 'time', 'amount']
data = [
(True, datetime(1970, 1, 1), Decimal("200.00"), 1),
(True, datetime(1970, 1, 1, 12, 30), Decimal("200.00"), 2),
(False, datetime(1970, 1, 2, 8, 45), Decimal("12.00"), 3)
]
df = spark.createDataFrame(data, schema=columns)
df.repartition(1).write.format('delta').partitionBy(
*partition_columns).save(case.delta_root)


@reference_table(
name='with_schema_change',
description='Table which has schema change using overwriteSchema=True.',
Expand All @@ -171,3 +193,110 @@ def with_schema_change(case: TestCaseInfo, spark: SparkSession):
'overwriteSchema', True).format('delta').save(
case.delta_root)
save_expected(case)

@reference_table(
name='all_primitive_types',
description='Table containing all non-nested types',
)
def create_all_primitive_types(case: TestCaseInfo, spark: SparkSession):
schema = types.StructType([
types.StructField("utf8", types.StringType()),
types.StructField("int64", types.LongType()),
types.StructField("int32", types.IntegerType()),
types.StructField("int16", types.ShortType()),
types.StructField("int8", types.ByteType()),
types.StructField("float32", types.FloatType()),
types.StructField("float64", types.DoubleType()),
types.StructField("bool", types.BooleanType()),
types.StructField("binary", types.BinaryType()),
types.StructField("decimal", types.DecimalType(5, 3)),
types.StructField("date32", types.DateType()),
types.StructField("timestamp", types.TimestampType()),
])

df = spark.createDataFrame([
(
str(i),
i,
i,
i,
i,
float(i),
float(i),
i % 2 == 0,
bytes(i),
Decimal("10.000") + i,
date(1970, 1, 1) + timedelta(days=i),
datetime(1970, 1, 1) + timedelta(hours=i)
)
for i in range(5)
], schema=schema)

df.repartition(1).write.format('delta').save(case.delta_root)


@reference_table(
name='nested_types',
description='Table containing various nested types',
)
def create_nested_types(case: TestCaseInfo, spark: SparkSession):
schema = types.StructType([
types.StructField("struct", types.StructType([
types.StructField("float64", types.DoubleType()),
types.StructField("bool", types.BooleanType()),
])),
types.StructField("array", types.ArrayType(types.ShortType())),
types.StructField("map", types.MapType(types.StringType(), types.IntegerType())),
])

df = spark.createDataFrame([
(
{ "float64": float(i), "bool": i % 2 == 0 },
list(range(i + 1)),
{ str(i): i for i in range(i) }
)
for i in range(5)
], schema=schema)

df.repartition(1).write.format('delta').save(case.delta_root)


def get_sample_data(spark: SparkSession, seed: int=42, nrows: int=5) -> pyspark.sql.DataFrame:
# Use seed to get consistent data between runs, for reproducibility
random.seed(seed)
return spark.createDataFrame([
(
random.choice(["a", "b", "c", None]),
random.randint(0, 1000),
date(random.randint(1970, 2020), random.randint(1, 12), 1)
)
for i in range(nrows)
], schema=["letter", "int", "date"])


@reference_table(
name='with_checkpoint',
description='Table with a checkpoint',
)
def create_with_checkpoint(case: TestCaseInfo, spark: SparkSession):
spark.conf.set("spark.databricks.delta.retentionDurationCheck.enabled", "false")

df = get_sample_data(spark)

table = (DeltaTable.create(spark)
.location(str(Path(case.delta_root).absolute()))
.addColumns(df.schema)
.property("delta.checkpointInterval", "2")
.property("delta.logRetentionDuration", "0 days")
.execute())

for i in range(5):
df = get_sample_data(spark, seed=i, nrows=5)
df.repartition(1).write.format('delta').mode('overwrite').save(case.delta_root)

assert any(path.suffixes == [".checkpoint", ".parquet"]
for path in (Path(case.delta_root) / "_delta_log").iterdir())

table.vacuum(retentionHours=0)


28 changes: 21 additions & 7 deletions dat/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import shutil
from pathlib import Path
from typing import Optional

import click

Expand Down Expand Up @@ -32,13 +33,26 @@ def cli():


@click.command()
def write_generated_reference_tables():
out_base = Path('out/reader_tests/generated')
shutil.rmtree(out_base)

for metadata, create_table in generated_tables.registered_reference_tables:
logging.info("Writing table '%s'", metadata.name)
create_table()
@click.option('--table-name')
def write_generated_reference_tables(table_name: Optional[str]):
if table_name:
for metadata, create_table in generated_tables.registered_reference_tables:
if metadata.name == table_name:
logging.info("Writing table '%s'", metadata.name)
out_base = Path('out/reader_tests/generated') / table_name
shutil.rmtree(out_base, ignore_errors=True)

create_table()
break
else:
raise ValueError(f"Could not find generated table named '{table_name}'")
else:
out_base = Path('out/reader_tests/generated')
shutil.rmtree(out_base)

for metadata, create_table in generated_tables.registered_reference_tables:
logging.info("Writing table '%s'", metadata.name)
create_table()


@click.command()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}
{"metaData":{"id":"90ceea80-98b2-4a16-99b4-bd8590371cc5","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"utf8\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"int64\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"int32\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"int16\",\"type\":\"short\",\"nullable\":true,\"metadata\":{}},{\"name\":\"int8\",\"type\":\"byte\",\"nullable\":true,\"metadata\":{}},{\"name\":\"float32\",\"type\":\"float\",\"nullable\":true,\"metadata\":{}},{\"name\":\"float64\",\"type\":\"double\",\"nullable\":true,\"metadata\":{}},{\"name\":\"bool\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}},{\"name\":\"binary\",\"type\":\"binary\",\"nullable\":true,\"metadata\":{}},{\"name\":\"decimal\",\"type\":\"decimal(5,3)\",\"nullable\":true,\"metadata\":{}},{\"name\":\"date32\",\"type\":\"date\",\"nullable\":true,\"metadata\":{}},{\"name\":\"timestamp\",\"type\":\"timestamp\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1668830585678}}
{"add":{"path":"part-00000-9a4d4f91-4cab-4294-9e26-97785c9556fc-c000.snappy.parquet","partitionValues":{},"size":3206,"modificationTime":1668830585964,"dataChange":true,"stats":"{\"numRecords\":5,\"minValues\":{\"utf8\":\"0\",\"int64\":0,\"int32\":0,\"int16\":0,\"int8\":0,\"float32\":0.0,\"float64\":0.0,\"decimal\":10.000,\"date32\":\"1970-01-01\",\"timestamp\":\"1970-01-01T00:00:00.000-08:00\"},\"maxValues\":{\"utf8\":\"4\",\"int64\":4,\"int32\":4,\"int16\":4,\"int8\":4,\"float32\":4.0,\"float64\":4.0,\"decimal\":14.000,\"date32\":\"1970-01-05\",\"timestamp\":\"1970-01-01T04:00:00.000-08:00\"},\"nullCount\":{\"utf8\":0,\"int64\":0,\"int32\":0,\"int16\":0,\"int8\":0,\"float32\":0,\"float64\":0,\"bool\":0,\"binary\":0,\"decimal\":0,\"date32\":0,\"timestamp\":0}}"}}
{"commitInfo":{"timestamp":1668830585986,"operation":"WRITE","operationParameters":{"mode":"ErrorIfExists","partitionBy":"[]"},"isolationLevel":"Serializable","isBlindAppend":true,"operationMetrics":{"numFiles":"1","numOutputRows":"5","numOutputBytes":"3206"},"engineInfo":"Apache-Spark/3.3.1 Delta-Lake/2.1.1","txnId":"46722e06-4b7f-463f-8608-94b227d9c98c"}}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 0,
"properties": {},
"min_reader_version": 1,
"min_writer_version": 2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "all_primitive_types",
"description": "Table containing all non-nested types"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}
{"metaData":{"id":"6908f940-d6f4-4020-8de8-fb62433f6164","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"letter\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"number\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"a_float\",\"type\":\"double\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1668830170207}}
{"add":{"path":"part-00000-3c9f01c2-6db4-4cbc-a744-dbc2f79eb78a-c000.snappy.parquet","partitionValues":{},"size":996,"modificationTime":1668830172404,"dataChange":true,"stats":"{\"numRecords\":3,\"minValues\":{\"letter\":\"a\",\"number\":1,\"a_float\":1.1},\"maxValues\":{\"letter\":\"c\",\"number\":3,\"a_float\":3.3},\"nullCount\":{\"letter\":0,\"number\":0,\"a_float\":0}}"}}
{"commitInfo":{"timestamp":1668830172528,"operation":"WRITE","operationParameters":{"mode":"ErrorIfExists","partitionBy":"[]"},"isolationLevel":"Serializable","isBlindAppend":true,"operationMetrics":{"numFiles":"1","numOutputRows":"3","numOutputBytes":"996"},"engineInfo":"Apache-Spark/3.3.1 Delta-Lake/2.1.1","txnId":"6d83d802-7374-4817-9cf1-1162039c26d5"}}
{"metaData":{"id":"d379f8f1-60b4-456c-a941-20ac267cbea3","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"letter\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"number\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"a_float\",\"type\":\"double\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1668830560516}}
{"add":{"path":"part-00000-7ced08f4-9725-4547-a3dd-1e39a1eaf7b0-c000.snappy.parquet","partitionValues":{},"size":996,"modificationTime":1668830562714,"dataChange":true,"stats":"{\"numRecords\":3,\"minValues\":{\"letter\":\"a\",\"number\":1,\"a_float\":1.1},\"maxValues\":{\"letter\":\"c\",\"number\":3,\"a_float\":3.3},\"nullCount\":{\"letter\":0,\"number\":0,\"a_float\":0}}"}}
{"commitInfo":{"timestamp":1668830562838,"operation":"WRITE","operationParameters":{"mode":"ErrorIfExists","partitionBy":"[]"},"isolationLevel":"Serializable","isBlindAppend":true,"operationMetrics":{"numFiles":"1","numOutputRows":"3","numOutputBytes":"996"},"engineInfo":"Apache-Spark/3.3.1 Delta-Lake/2.1.1","txnId":"92a218de-ba83-4830-b62a-caa0ee1e35ad"}}
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
{"add":{"path":"part-00000-4ae8c002-c1fa-4afe-abaa-75e0962184e0-c000.snappy.parquet","partitionValues":{},"size":984,"modificationTime":1668830177704,"dataChange":true,"stats":"{\"numRecords\":2,\"minValues\":{\"letter\":\"d\",\"number\":4,\"a_float\":4.4},\"maxValues\":{\"letter\":\"e\",\"number\":5,\"a_float\":5.5},\"nullCount\":{\"letter\":0,\"number\":0,\"a_float\":0}}"}}
{"commitInfo":{"timestamp":1668830177722,"operation":"WRITE","operationParameters":{"mode":"Append","partitionBy":"[]"},"readVersion":0,"isolationLevel":"Serializable","isBlindAppend":true,"operationMetrics":{"numFiles":"1","numOutputRows":"2","numOutputBytes":"984"},"engineInfo":"Apache-Spark/3.3.1 Delta-Lake/2.1.1","txnId":"63219ee7-54b4-491c-897f-0482e27b513a"}}
{"add":{"path":"part-00000-127f8ce7-badc-4b8e-b585-674644c96f90-c000.snappy.parquet","partitionValues":{},"size":984,"modificationTime":1668830568044,"dataChange":true,"stats":"{\"numRecords\":2,\"minValues\":{\"letter\":\"d\",\"number\":4,\"a_float\":4.4},\"maxValues\":{\"letter\":\"e\",\"number\":5,\"a_float\":5.5},\"nullCount\":{\"letter\":0,\"number\":0,\"a_float\":0}}"}}
{"commitInfo":{"timestamp":1668830568065,"operation":"WRITE","operationParameters":{"mode":"Append","partitionBy":"[]"},"readVersion":0,"isolationLevel":"Serializable","isBlindAppend":true,"operationMetrics":{"numFiles":"1","numOutputRows":"2","numOutputBytes":"984"},"engineInfo":"Apache-Spark/3.3.1 Delta-Lake/2.1.1","txnId":"53ffff03-f331-461a-aa26-0e1aa244f2ca"}}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}
{"metaData":{"id":"f743ad68-9fda-4994-a81f-379447341471","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"letter\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"number\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"a_float\",\"type\":\"double\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["letter"],"configuration":{},"createdTime":1668830180174}}
{"add":{"path":"letter=a/part-00000-7453e46f-e8a5-42ea-8f14-aabb437a76a1.c000.snappy.parquet","partitionValues":{"letter":"a"},"size":751,"modificationTime":1668830180584,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":1,\"a_float\":1.1},\"maxValues\":{\"number\":1,\"a_float\":1.1},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"add":{"path":"letter=b/part-00000-4392475f-604d-4659-8b26-56b99f214d1e.c000.snappy.parquet","partitionValues":{"letter":"b"},"size":751,"modificationTime":1668830180594,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":2,\"a_float\":2.2},\"maxValues\":{\"number\":2,\"a_float\":2.2},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"add":{"path":"letter=c/part-00000-b1667a28-077a-4b99-8f98-c6bc63659e83.c000.snappy.parquet","partitionValues":{"letter":"c"},"size":751,"modificationTime":1668830180604,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":3,\"a_float\":3.3},\"maxValues\":{\"number\":3,\"a_float\":3.3},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"commitInfo":{"timestamp":1668830180626,"operation":"WRITE","operationParameters":{"mode":"ErrorIfExists","partitionBy":"[\"letter\"]"},"isolationLevel":"Serializable","isBlindAppend":true,"operationMetrics":{"numFiles":"3","numOutputRows":"3","numOutputBytes":"2253"},"engineInfo":"Apache-Spark/3.3.1 Delta-Lake/2.1.1","txnId":"5f4fd18f-5ffd-49d8-8edd-57706758c8f7"}}
{"metaData":{"id":"755af727-900c-4f84-a24a-221b84e8f118","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"letter\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"number\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}},{\"name\":\"a_float\",\"type\":\"double\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["letter"],"configuration":{},"createdTime":1668830570542}}
{"add":{"path":"letter=a/part-00000-620b9d86-2586-444e-8c2b-758e38e0fea8.c000.snappy.parquet","partitionValues":{"letter":"a"},"size":751,"modificationTime":1668830570934,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":1,\"a_float\":1.1},\"maxValues\":{\"number\":1,\"a_float\":1.1},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"add":{"path":"letter=b/part-00000-0be3b8ce-bed0-437d-a1ef-fbde2f7ebd42.c000.snappy.parquet","partitionValues":{"letter":"b"},"size":751,"modificationTime":1668830570944,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":2,\"a_float\":2.2},\"maxValues\":{\"number\":2,\"a_float\":2.2},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"add":{"path":"letter=c/part-00000-8f5d6eea-fe56-4ae0-8ea2-73ed6a8f0fd2.c000.snappy.parquet","partitionValues":{"letter":"c"},"size":751,"modificationTime":1668830570954,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":3,\"a_float\":3.3},\"maxValues\":{\"number\":3,\"a_float\":3.3},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"commitInfo":{"timestamp":1668830570977,"operation":"WRITE","operationParameters":{"mode":"ErrorIfExists","partitionBy":"[\"letter\"]"},"isolationLevel":"Serializable","isBlindAppend":true,"operationMetrics":{"numFiles":"3","numOutputRows":"3","numOutputBytes":"2253"},"engineInfo":"Apache-Spark/3.3.1 Delta-Lake/2.1.1","txnId":"a5f10439-c065-43cc-bd1a-576ae5c2d808"}}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{"add":{"path":"letter=__HIVE_DEFAULT_PARTITION__/part-00000-07603d15-4f67-47d1-afaa-2d7b29cc3614.c000.snappy.parquet","partitionValues":{"letter":null},"size":751,"modificationTime":1668830182584,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":6,\"a_float\":6.6},\"maxValues\":{\"number\":6,\"a_float\":6.6},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"add":{"path":"letter=a/part-00000-cf134b04-a0a9-4d4a-a53c-4feb53f10315.c000.snappy.parquet","partitionValues":{"letter":"a"},"size":751,"modificationTime":1668830182634,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":4,\"a_float\":4.4},\"maxValues\":{\"number\":4,\"a_float\":4.4},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"add":{"path":"letter=e/part-00000-48e8fe71-17f2-4e92-a792-9c67ef1fd8b2.c000.snappy.parquet","partitionValues":{"letter":"e"},"size":750,"modificationTime":1668830182634,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":5,\"a_float\":5.5},\"maxValues\":{\"number\":5,\"a_float\":5.5},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"commitInfo":{"timestamp":1668830182658,"operation":"WRITE","operationParameters":{"mode":"Append","partitionBy":"[\"letter\"]"},"readVersion":0,"isolationLevel":"Serializable","isBlindAppend":true,"operationMetrics":{"numFiles":"3","numOutputRows":"3","numOutputBytes":"2252"},"engineInfo":"Apache-Spark/3.3.1 Delta-Lake/2.1.1","txnId":"4ea7bc7d-4a92-4aae-94df-64f28ecdcc1e"}}
{"add":{"path":"letter=__HIVE_DEFAULT_PARTITION__/part-00000-4f282cf1-dc7e-48df-8091-cd5a7f7be0cf.c000.snappy.parquet","partitionValues":{"letter":null},"size":751,"modificationTime":1668830573074,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":6,\"a_float\":6.6},\"maxValues\":{\"number\":6,\"a_float\":6.6},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"add":{"path":"letter=a/part-00000-213acc83-9e68-4366-bdc3-9074170c3b1f.c000.snappy.parquet","partitionValues":{"letter":"a"},"size":751,"modificationTime":1668830573084,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":4,\"a_float\":4.4},\"maxValues\":{\"number\":4,\"a_float\":4.4},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"add":{"path":"letter=e/part-00000-02057cfc-7ca6-4930-bcd5-9528fe59d7cb.c000.snappy.parquet","partitionValues":{"letter":"e"},"size":750,"modificationTime":1668830573094,"dataChange":true,"stats":"{\"numRecords\":1,\"minValues\":{\"number\":5,\"a_float\":5.5},\"maxValues\":{\"number\":5,\"a_float\":5.5},\"nullCount\":{\"number\":0,\"a_float\":0}}"}}
{"commitInfo":{"timestamp":1668830573108,"operation":"WRITE","operationParameters":{"mode":"Append","partitionBy":"[\"letter\"]"},"readVersion":0,"isolationLevel":"Serializable","isBlindAppend":true,"operationMetrics":{"numFiles":"3","numOutputRows":"3","numOutputBytes":"2252"},"engineInfo":"Apache-Spark/3.3.1 Delta-Lake/2.1.1","txnId":"d79e464f-a050-4914-81bf-9e92ca7dcf35"}}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit de68fc8

Please sign in to comment.