CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
143 lines
5.3 KiB
Python
143 lines
5.3 KiB
Python
"""AtomicJsonFile — R02-T01. Test tiêm lỗi, đúng như cột nghiệm thu của plan.md.
|
|
|
|
Cách kiểm: cắt ngang giữa lúc ghi rồi khẳng định file cũ **còn nguyên**. Nếu
|
|
chỉ test "ghi rồi đọc lại thấy đúng" thì `path.write_text()` cũ cũng qua — mà
|
|
đó chính là thứ ta đang thay.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from cowork_local.infrastructure.persistence.json.atomic_json_file import AtomicJsonFile
|
|
|
|
|
|
def test_ghi_roi_doc_lai(tmp_path):
|
|
f = AtomicJsonFile(tmp_path / "cau_hinh.json")
|
|
f.write({"theme": "dark", "ngôn ngữ": "vi"})
|
|
assert f.read() == {"theme": "dark", "ngôn ngữ": "vi"}
|
|
|
|
|
|
def test_chua_co_file_thi_tra_mac_dinh(tmp_path):
|
|
f = AtomicJsonFile(tmp_path / "chua-ton-tai.json")
|
|
assert f.read(default={"theme": "dark"}) == {"theme": "dark"}
|
|
assert f.exists() is False
|
|
|
|
|
|
def test_chet_giua_luc_ghi_thi_file_cu_con_nguyen(tmp_path, monkeypatch):
|
|
"""Lõi của R02-T01.
|
|
|
|
Giả lập mất điện đúng lúc: cho ``os.replace`` ném lỗi. Đây là bước cuối
|
|
cùng, tức là dữ liệu mới đã nằm trong file tạm rồi — nếu cài đặt sai theo
|
|
kiểu ghi đè thẳng, file đích lúc này đã hỏng.
|
|
"""
|
|
path = tmp_path / "cau_hinh.json"
|
|
f = AtomicJsonFile(path)
|
|
f.write({"phiên bản": 1, "quan trọng": "đừng mất"})
|
|
|
|
def no_dien(*args, **kwargs):
|
|
raise OSError("mô phỏng mất điện")
|
|
|
|
monkeypatch.setattr(os, "replace", no_dien)
|
|
with pytest.raises(OSError):
|
|
f.write({"phiên bản": 2})
|
|
|
|
# bản cũ phải còn y nguyên
|
|
assert f.read() == {"phiên bản": 1, "quan trọng": "đừng mất"}
|
|
|
|
|
|
def test_khong_de_lai_rac_tmp_khi_ghi_hong(tmp_path, monkeypatch):
|
|
path = tmp_path / "cau_hinh.json"
|
|
f = AtomicJsonFile(path)
|
|
f.write({"a": 1})
|
|
|
|
monkeypatch.setattr(os, "replace", lambda *a, **k: (_ for _ in ()).throw(OSError("x")))
|
|
with pytest.raises(OSError):
|
|
f.write({"a": 2})
|
|
|
|
con_lai = [p.name for p in tmp_path.iterdir()]
|
|
assert con_lai == ["cau_hinh.json"], f"còn rác: {con_lai}"
|
|
|
|
|
|
def test_file_hong_thi_cach_ly_va_tra_mac_dinh(tmp_path):
|
|
"""Hỏng cấu hình không được chặn khởi động — giữ đúng hành vi config.py
|
|
hiện tại, nhưng thêm phần giữ lại bản hỏng để còn cứu."""
|
|
path = tmp_path / "cau_hinh.json"
|
|
path.write_text("{ đây không phải json", encoding="utf-8")
|
|
f = AtomicJsonFile(path)
|
|
|
|
assert f.read(default={"theme": "dark"}) == {"theme": "dark"}
|
|
assert not path.exists(), "file hỏng phải được dời đi"
|
|
bad = list(tmp_path.glob("*.bad-*"))
|
|
assert len(bad) == 1, "phải giữ lại bản hỏng để cứu tay"
|
|
assert "đây không phải json" in bad[0].read_text(encoding="utf-8")
|
|
|
|
|
|
def test_ghi_de_nhieu_lan_van_dung(tmp_path):
|
|
f = AtomicJsonFile(tmp_path / "dem.json")
|
|
for i in range(20):
|
|
f.write({"lần": i})
|
|
assert f.read() == {"lần": 19}
|
|
assert list(tmp_path.glob("*.tmp")) == []
|
|
|
|
|
|
def test_giu_nguyen_tieng_viet_khong_escape(tmp_path):
|
|
"""config.py hiện dùng ensure_ascii=False — giữ nguyên để file đọc được
|
|
bằng mắt và git diff không thành một đống \\uXXXX."""
|
|
path = tmp_path / "vi.json"
|
|
AtomicJsonFile(path).write({"tên": "Nguyễn Văn Đức"})
|
|
raw = path.read_text(encoding="utf-8")
|
|
assert "Nguyễn Văn Đức" in raw
|
|
assert "\\u" not in raw
|
|
|
|
|
|
def test_tao_thu_muc_cha_neu_chua_co(tmp_path):
|
|
f = AtomicJsonFile(tmp_path / "sâu" / "hơn" / "nữa" / "c.json")
|
|
f.write({"ok": True})
|
|
assert f.read() == {"ok": True}
|
|
|
|
|
|
def test_json_ghi_ra_doc_duoc_bang_thu_vien_chuan(tmp_path):
|
|
path = tmp_path / "c.json"
|
|
AtomicJsonFile(path).write({"n": [1, 2, {"m": None}]})
|
|
assert json.loads(path.read_text(encoding="utf-8")) == {"n": [1, 2, {"m": None}]}
|
|
|
|
|
|
# ---- Windows: os.replace bị Defender/Indexer chặn tạm thời -----------------
|
|
|
|
def test_thu_lai_khi_windows_chan_tam_thoi(tmp_path, monkeypatch):
|
|
"""Hỏng 2 lần đầu rồi thành công — phải ghi được, không ném lỗi.
|
|
|
|
Đây là lỗi thật bắt được ngày 25/08: chạy vòng 20 lần ghi thì cứ 7 lượt
|
|
lại có 1 lượt văng ``PermissionError: [WinError 5]`` ở ``os.replace``.
|
|
"""
|
|
that = os.replace
|
|
con_hong = [2]
|
|
|
|
def replace_do_dong(src, dst):
|
|
if con_hong[0]:
|
|
con_hong[0] -= 1
|
|
raise PermissionError(5, "Access is denied")
|
|
return that(src, dst)
|
|
|
|
monkeypatch.setattr(os, "replace", replace_do_dong)
|
|
AtomicJsonFile(tmp_path / "a.json").write({"x": 1})
|
|
|
|
assert con_hong[0] == 0, "phải thật sự có thử lại, không phải may mà qua"
|
|
assert json.loads((tmp_path / "a.json").read_text(encoding="utf-8")) == {"x": 1}
|
|
assert list(tmp_path.glob("*.tmp")) == []
|
|
|
|
|
|
def test_hong_that_thi_van_nem_loi_va_khong_de_lai_rac(tmp_path, monkeypatch):
|
|
"""Thử lại không được phép nuốt lỗi quyền thật — hết lượt là ném."""
|
|
def luon_hong(src, dst):
|
|
raise PermissionError(5, "Access is denied")
|
|
|
|
monkeypatch.setattr(os, "replace", luon_hong)
|
|
with pytest.raises(PermissionError):
|
|
AtomicJsonFile(tmp_path / "b.json").write({"x": 1})
|
|
|
|
assert list(tmp_path.glob("*.tmp")) == [], "phải dọn file tạm"
|