Skip to content

Commit f273813

Browse files
committed
add tests for file_utils and fix import_file_checksum to allow sqlite support
1 parent 3f466a1 commit f273813

File tree

2 files changed

+296
-5
lines changed

2 files changed

+296
-5
lines changed

libqfieldsync/utils/file_utils.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,13 @@ def open_folder(path: Union[Path, str]) -> None:
115115

116116
def import_file_checksum(path: str) -> Optional[str]:
117117
md5sum = None
118-
path = os.path.join(path, "data.gpkg")
119-
if not os.path.exists(path):
120-
path = os.path.join(path, "data.sqlite")
121-
if os.path.exists(path):
122-
with open(path, "rb") as f:
118+
119+
file_path = os.path.join(path, "data.gpkg")
120+
if not os.path.exists(file_path):
121+
file_path = os.path.join(path, "data.sqlite")
122+
123+
if os.path.exists(file_path):
124+
with open(file_path, "rb") as f:
123125
file_data = f.read()
124126
# TODO @suricactus: Python 3.9, pass `usedforsecurity=False`
125127
# https://app.clickup.com/t/2192114/QF-6481

tests/test_file_utils.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import hashlib
2+
from pathlib import Path
3+
4+
import pytest
5+
from qgis.testing import start_app
6+
7+
from libqfieldsync.utils.exceptions import NoProjectFoundError, QFieldSyncError
8+
from libqfieldsync.utils.file_utils import (
9+
copy_additional_project_files,
10+
copy_attachments,
11+
copy_multifile,
12+
fileparts,
13+
get_children_with_extension,
14+
get_full_parent_path,
15+
get_project_in_folder,
16+
get_project_like_files,
17+
get_unique_empty_dirname,
18+
import_file_checksum,
19+
is_valid_filename,
20+
is_valid_filepath,
21+
isascii,
22+
open_folder,
23+
slugify,
24+
)
25+
26+
start_app()
27+
28+
29+
def test_fileparts_with_and_without_extension_dot():
30+
assert fileparts("/path/example/project.qgs") == (
31+
"/path/example",
32+
"project",
33+
".qgs",
34+
)
35+
assert fileparts("/path/example/project.qgs", extension_dot=False) == (
36+
"/path/example",
37+
"project",
38+
"qgs",
39+
)
40+
41+
42+
def test_get_children_with_extension_returns_expected_matches(tmp_path):
43+
parent = tmp_path.joinpath("parent")
44+
parent.mkdir()
45+
parent.joinpath("project.qgs").write_text("")
46+
parent.joinpath("other.txt").write_text("")
47+
48+
assert get_children_with_extension(str(parent), "qgs") == [
49+
str(parent.joinpath("project.qgs"))
50+
]
51+
52+
53+
def test_get_children_with_extension_raises_for_missing_directory(tmp_path):
54+
with pytest.raises(QFieldSyncError):
55+
get_children_with_extension(str(tmp_path.joinpath("missing")), "qgs")
56+
57+
58+
def test_get_children_with_extension_raises_for_unexpected_match_count(tmp_path):
59+
parent = tmp_path.joinpath("parent")
60+
parent.mkdir()
61+
parent.joinpath("a.qgs").write_text("")
62+
parent.joinpath("b.qgs").write_text("")
63+
64+
with pytest.raises(QFieldSyncError):
65+
get_children_with_extension(str(parent), "qgs", count=1)
66+
67+
68+
def test_get_full_parent_path_normalizes_parent(tmp_path):
69+
path = tmp_path.joinpath("nested", "..", "folder", "project.qgs")
70+
71+
assert get_full_parent_path(str(path)) == str(tmp_path.joinpath("folder"))
72+
73+
74+
def test_get_project_in_folder_returns_project(tmp_path):
75+
parent = tmp_path.joinpath("project")
76+
parent.mkdir()
77+
project_file = parent.joinpath("project.qgs")
78+
project_file.write_text("")
79+
80+
assert get_project_in_folder(str(parent)) == str(project_file)
81+
82+
83+
def test_get_project_in_folder_raises_when_missing(tmp_path):
84+
parent = tmp_path.joinpath("project")
85+
parent.mkdir()
86+
87+
with pytest.raises(NoProjectFoundError):
88+
get_project_in_folder(str(parent))
89+
90+
91+
def test_open_folder_uses_windows_explorer(tmp_path, monkeypatch):
92+
calls = []
93+
monkeypatch.setattr(
94+
"libqfieldsync.utils.file_utils.platform.system", lambda: "Windows"
95+
)
96+
monkeypatch.setattr("libqfieldsync.utils.file_utils.subprocess.Popen", calls.append)
97+
98+
path = tmp_path.joinpath("folder")
99+
open_folder(path)
100+
101+
assert calls == [rf'explorer /select,"{path}"']
102+
103+
104+
def test_open_folder_uses_macos_open(tmp_path, monkeypatch):
105+
calls = []
106+
monkeypatch.setattr(
107+
"libqfieldsync.utils.file_utils.platform.system", lambda: "Darwin"
108+
)
109+
monkeypatch.setattr("libqfieldsync.utils.file_utils.subprocess.Popen", calls.append)
110+
111+
path = tmp_path.joinpath("folder")
112+
open_folder(path)
113+
114+
assert calls == [["open", "-R", path]]
115+
116+
117+
def test_open_folder_uses_xdg_open(tmp_path, monkeypatch):
118+
calls = []
119+
monkeypatch.setattr(
120+
"libqfieldsync.utils.file_utils.platform.system", lambda: "Linux"
121+
)
122+
monkeypatch.setattr("libqfieldsync.utils.file_utils.subprocess.Popen", calls.append)
123+
124+
path = tmp_path.joinpath("folder")
125+
open_folder(path)
126+
127+
assert calls == [["xdg-open", path]]
128+
129+
130+
def test_open_folder_uses_xdg_open_for_unknown_os(tmp_path, monkeypatch):
131+
calls = []
132+
monkeypatch.setattr(
133+
"libqfieldsync.utils.file_utils.platform.system", lambda: "FreeBSD"
134+
)
135+
monkeypatch.setattr("libqfieldsync.utils.file_utils.subprocess.Popen", calls.append)
136+
137+
path = tmp_path.joinpath("folder")
138+
open_folder(path)
139+
140+
assert calls == [["xdg-open", path]]
141+
142+
143+
def test_import_file_checksum_prefers_gpkg_and_falls_back_to_sqlite(tmp_path):
144+
gpkg_dir = tmp_path.joinpath("gpkg")
145+
gpkg_dir.mkdir()
146+
gpkg_bytes = b"gpkg data"
147+
gpkg_dir.joinpath("data.gpkg").write_bytes(gpkg_bytes)
148+
149+
sqlite_dir = tmp_path.joinpath("sqlite")
150+
sqlite_dir.mkdir()
151+
sqlite_bytes = b"sqlite data"
152+
sqlite_dir.joinpath("data.sqlite").write_bytes(sqlite_bytes)
153+
154+
assert (
155+
import_file_checksum(str(gpkg_dir))
156+
== hashlib.md5(gpkg_bytes, usedforsecurity=False).hexdigest()
157+
)
158+
assert (
159+
import_file_checksum(str(sqlite_dir))
160+
== hashlib.md5(sqlite_bytes, usedforsecurity=False).hexdigest()
161+
)
162+
assert import_file_checksum(str(tmp_path.joinpath("missing"))) is None
163+
164+
165+
def test_slugify_normalizes_unicode_and_separators():
166+
assert slugify("Café déjà vu / Layer 01") == "cafe-de-ja-vu-layer-01"
167+
168+
169+
def test_copy_attachments_copies_nested_tree(tmp_path):
170+
source_root = tmp_path.joinpath("source")
171+
dest_root = tmp_path.joinpath("dest")
172+
attachments_dir = source_root.joinpath("DCIM", "subfolder")
173+
attachments_dir.mkdir(parents=True)
174+
source_root.joinpath("DCIM", "photo.jpg").write_text("image")
175+
attachments_dir.joinpath("nested.jpg").write_text("nested")
176+
177+
copy_attachments(source_root, dest_root, Path("DCIM"))
178+
179+
assert (
180+
dest_root.joinpath("DCIM", "photo.jpg").read_text(encoding="utf-8") == "image"
181+
)
182+
assert (
183+
dest_root.joinpath("DCIM", "subfolder", "nested.jpg").read_text(
184+
encoding="utf-8"
185+
)
186+
== "nested"
187+
)
188+
189+
190+
def test_copy_multifile_copies_gpkg_sidecars(tmp_path):
191+
source = tmp_path.joinpath("source.gpkg")
192+
dest = tmp_path.joinpath("dest.gpkg")
193+
source.write_text("main")
194+
tmp_path.joinpath("source.gpkg-shm").write_text("shm")
195+
tmp_path.joinpath("source.gpkg-wal").write_text("wal")
196+
197+
copy_multifile(source, dest)
198+
199+
assert dest.read_text(encoding="utf-8") == "main"
200+
assert tmp_path.joinpath("dest.gpkg-shm").read_text(encoding="utf-8") == "shm"
201+
assert tmp_path.joinpath("dest.gpkg-wal").read_text(encoding="utf-8") == "wal"
202+
203+
204+
def test_get_unique_empty_dirname_reuses_empty_and_increments_non_empty(tmp_path):
205+
base = tmp_path.joinpath("export")
206+
assert get_unique_empty_dirname(base) == base
207+
208+
base.mkdir()
209+
assert get_unique_empty_dirname(base) == base
210+
211+
base.joinpath("file.txt").write_text("data")
212+
base_1 = tmp_path.joinpath("export_1")
213+
base_1.mkdir()
214+
base_1.joinpath("file.txt").write_text("data")
215+
216+
assert get_unique_empty_dirname(base) == tmp_path.joinpath("export_2")
217+
218+
219+
def test_isascii_and_path_validation_helpers():
220+
assert isascii("plain-file.txt") is True
221+
assert isascii("ümlaut.txt") is False
222+
assert isascii("ки/ри/ли/ца.txt") is False
223+
assert is_valid_filename("valid-name_01.txt") is True
224+
assert is_valid_filename("bad:name.txt") is False
225+
assert is_valid_filename("CON") is False
226+
assert is_valid_filepath("folder/subfolder/file.txt") is True
227+
assert is_valid_filepath("folder/bad:name.txt") is False
228+
229+
230+
def test_get_project_like_files_returns_matching_sidecars(tmp_path):
231+
project = tmp_path.joinpath("my.project.qgs")
232+
project.write_text("")
233+
qml_file = tmp_path.joinpath("my.project.qml")
234+
qml_file.write_text("")
235+
qm_file = tmp_path.joinpath("my.project_de.qm")
236+
qm_file.write_text("")
237+
tmp_path.joinpath("otherproject.qml").write_text("")
238+
239+
assert sorted(get_project_like_files(project, ".*")) == sorted(
240+
[str(project), str(qml_file)]
241+
)
242+
assert get_project_like_files(project, "_??.qm") == [str(qm_file)]
243+
244+
245+
def test_copy_additional_project_files_renames_qml_and_qm_files(tmp_path):
246+
source_dir = tmp_path.joinpath("source")
247+
export_dir = tmp_path.joinpath("export")
248+
source_dir.mkdir()
249+
export_dir.mkdir()
250+
251+
source_project = source_dir.joinpath("project.qgs")
252+
export_project = export_dir.joinpath("project_qfield.qgs")
253+
source_project.write_text("")
254+
export_project.write_text("")
255+
256+
plugin_file = source_dir.joinpath("project.qml")
257+
translation_file = source_dir.joinpath("project_de.qm")
258+
nested_translation_file = source_dir.joinpath("i18n", "project_fr.qm")
259+
asset_file = source_dir.joinpath("notes.txt")
260+
261+
nested_translation_file.parent.mkdir()
262+
plugin_file.write_text("plugin")
263+
translation_file.write_text("de")
264+
nested_translation_file.write_text("fr")
265+
asset_file.write_text("notes")
266+
267+
copy_additional_project_files(
268+
source_project,
269+
export_project,
270+
[
271+
str(plugin_file),
272+
str(translation_file),
273+
str(nested_translation_file),
274+
str(asset_file),
275+
],
276+
)
277+
278+
assert (
279+
export_dir.joinpath("project_qfield.qml").read_text(encoding="utf-8")
280+
== "plugin"
281+
)
282+
assert (
283+
export_dir.joinpath("project_qfield_de.qm").read_text(encoding="utf-8") == "de"
284+
)
285+
assert (
286+
export_dir.joinpath("i18n", "project_qfield_fr.qm").read_text(encoding="utf-8")
287+
== "fr"
288+
)
289+
assert export_dir.joinpath("notes.txt").read_text(encoding="utf-8") == "notes"

0 commit comments

Comments
 (0)