Skip to content

Commit 1d4984b

Browse files
committed
tests: Add fixture to create content directory
The `create_contentdir` function takes the fake channel DB JSON files and creates a content directory with the channel databases and content files for testing. That's made available as the `contentdir` pytest fixture. A standalone script is provided as a convenience and for exercising the functionality outside of the test suite.
1 parent 6c3180a commit 1d4984b

File tree

3 files changed

+163
-0
lines changed

3 files changed

+163
-0
lines changed

kolibri_explore_plugin/test/conftest.py

+16
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,24 @@
55
import pytest
66
from django.core.management import call_command
77

8+
from .utils import create_contentdir
9+
810

911
@pytest.fixture
1012
def content_data(db):
1113
"""Load test content database fixture"""
1214
call_command("loaddata", "test-content.json")
15+
16+
17+
@pytest.fixture(scope="session")
18+
def serverdir(tmp_path_factory):
19+
"""Session scoped server root directory"""
20+
return tmp_path_factory.mktemp("server")
21+
22+
23+
@pytest.fixture(scope="session")
24+
def contentdir(serverdir):
25+
"""Session scoped server content directory"""
26+
contentdir = serverdir / "content"
27+
create_contentdir(contentdir)
28+
return contentdir

kolibri_explore_plugin/test/utils.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Test utilities
2+
#
3+
# Copyright 2023 Endless OS Foundation LLC
4+
# SPDX-License-Identifier: GPL-2.0-or-later
5+
import json
6+
import logging
7+
import os
8+
from base64 import b64decode
9+
from glob import iglob
10+
from hashlib import md5
11+
from pathlib import Path
12+
13+
logger = logging.getLogger(__name__)
14+
15+
TESTDIR = Path(__file__).parent.resolve()
16+
CHANNELSDIR = TESTDIR / "channels"
17+
18+
19+
def create_contentdir(content_path, channels_path=CHANNELSDIR):
20+
"""Create content directory from channel JSON files"""
21+
from kolibri.core.content.constants.schema_versions import (
22+
CURRENT_SCHEMA_VERSION,
23+
)
24+
from kolibri.core.content.utils.sqlalchemybridge import Bridge
25+
26+
databases_path = content_path / "databases"
27+
databases_path.mkdir(parents=True, exist_ok=True)
28+
storage_path = content_path / "storage"
29+
storage_path.mkdir(parents=True, exist_ok=True)
30+
31+
for json_path in iglob(f"{channels_path}/*.json"):
32+
logger.info(f"Loading channel JSON {json_path}")
33+
with open(json_path, "r") as f:
34+
data = json.load(f)
35+
36+
channels = data["content_channelmetadata"]
37+
if len(channels) != 1:
38+
raise ValueError(
39+
"Must be one channel in content_channelmetadata table"
40+
)
41+
42+
channel_id = channels[0]["id"]
43+
db_path = databases_path / f"{channel_id}.sqlite3"
44+
if db_path.exists():
45+
logger.info(f"Removing existing channel database {db_path}")
46+
db_path.unlink()
47+
48+
logger.info(f"Creating channel database {db_path}")
49+
bridge = Bridge(db_path, schema_version=CURRENT_SCHEMA_VERSION)
50+
bridge.Base.metadata.bind = bridge.engine
51+
bridge.Base.metadata.create_all()
52+
53+
# Create the content files from the localfile _content entries.
54+
logger.info(f"Creating channel {channel_id} content files")
55+
for localfile in data["content_localfile"]:
56+
id = localfile["id"]
57+
size = localfile["file_size"]
58+
ext = localfile["extension"]
59+
content = b64decode(localfile.pop("_content"))
60+
content_size = len(content)
61+
if content_size != size:
62+
raise ValueError(
63+
f"Localfile {id} size {size} does not match "
64+
f"content size {content_size}"
65+
)
66+
content_md5 = md5(content).hexdigest()
67+
if content_md5 != id:
68+
raise ValueError(
69+
f"Localfile {id} does not match content md5sum "
70+
f"{content_md5}"
71+
)
72+
73+
# If the file already exists, validate its contents. Otherwise,
74+
# create it.
75+
localfile_dir = storage_path / f"{id[0]}/{id[1]}"
76+
localfile_dir.mkdir(parents=True, exist_ok=True)
77+
localfile_path = localfile_dir / f"{id}.{ext}"
78+
if localfile_path.exists():
79+
logger.info(f"Validating content file {localfile_path}")
80+
localfile_size = os.path.getsize(localfile_path)
81+
if localfile_size != size:
82+
raise ValueError(
83+
f"Localfile {id} size {size} does not match "
84+
f"{localfile_path} size {localfile_size}"
85+
)
86+
with open(localfile_path, "rb") as f:
87+
localfile_content = f.read()
88+
if localfile_content != content:
89+
raise ValueError(
90+
f"Localfile {id} content does not match "
91+
f"{localfile_path} content"
92+
)
93+
else:
94+
logger.debug(f"Creating content file {localfile_path}")
95+
with open(localfile_path, "wb") as f:
96+
f.write(content)
97+
98+
# Now fill all the database tables.
99+
for table in bridge.Base.metadata.sorted_tables:
100+
if data.get(table.name):
101+
bridge.connection.execute(table.insert(), data[table.name])
102+
103+
bridge.end()

scripts/create_contentdir.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2023 Endless OS Foundation LLC
3+
# SPDX-License-Identifier: GPL-2.0-or-later
4+
import os
5+
from argparse import ArgumentParser
6+
from pathlib import Path
7+
from tempfile import TemporaryDirectory
8+
9+
from kolibri_explore_plugin.test.utils import create_contentdir
10+
11+
SRCDIR = Path(__file__).parent.parent
12+
CHANNELSDIR = SRCDIR / "kolibri_explore_plugin/test/channels"
13+
14+
15+
def main():
16+
ap = ArgumentParser(description="Create channel database from JSON")
17+
ap.add_argument(
18+
"contentdir",
19+
metavar="CONTENTDIR",
20+
type=Path,
21+
help="content output directory",
22+
)
23+
ap.add_argument(
24+
"-c",
25+
"--channelsdir",
26+
metavar="CHANNELSDIR",
27+
type=Path,
28+
default=CHANNELSDIR,
29+
help="test channels directory (default: %(default)s)",
30+
)
31+
args = ap.parse_args()
32+
33+
# Unfortunately, sqlalchemybridge can't be used without an initialized
34+
# Kolibri homedir, so make a temporary one.
35+
with TemporaryDirectory(prefix="kolibri-home-") as kolibri_home:
36+
os.environ["KOLIBRI_HOME"] = kolibri_home
37+
from kolibri.main import initialize
38+
39+
initialize()
40+
create_contentdir(args.contentdir, args.channelsdir)
41+
42+
43+
if __name__ == "__main__":
44+
main()

0 commit comments

Comments
 (0)