Coverage for pytest_recap/storage.py: 31%

36 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2025-05-05 06:24 -0600

1import json 

2from pathlib import Path 

3from typing import List, Optional 

4 

5from filelock import FileLock 

6 

7 

8class JSONStorage: 

9 """ 

10 Stores test sessions in a local JSON file, supporting both single-session (dict) and multi-session (list) modes. 

11 - Single-session mode (used by the pytest plugin): writes a single session as a dict, overwriting the file. 

12 - Multi-session/archive mode: appends sessions to a list, allowing for archival of multiple sessions in one file. 

13 - Thread/process-safe via file locking. 

14 

15 Args: 

16 file_path (Optional[str]): Path to the JSON file. Defaults to ~/.pytest_recap/sessions.json 

17 

18 Methods: 

19 save_session(session_data: dict, single: bool = False): 

20 Appends session_data to the file as a list (default), or overwrites as a dict if single=True. 

21 save_single_session(session_data: dict): 

22 Overwrites the file with a single session dict (for plugin recap output). 

23 load_sessions(lock: bool = True) -> List[dict]: 

24 Loads all sessions as a list (returns [] if file is a dict or empty). 

25 

26 Example usage: 

27 storage = JSONStorage(file_path="sessions.json") 

28 storage.save_session(session_dict) # archive mode 

29 storage.save_single_session(session_dict) # single recap file 

30 """ 

31 

32 def __init__(self, file_path: Optional[str] = None): 

33 self.file_path = Path(file_path) if file_path else Path.home() / ".pytest_recap" / "sessions.json" 

34 self.file_path.parent.mkdir(parents=True, exist_ok=True) 

35 self.lock_path = f"{self.file_path}.lock" 

36 if not self.file_path.exists(): 

37 # Only lock here if other processes could create at the same time 

38 with FileLock(self.lock_path): 

39 self._write_json([]) 

40 

41 def save_session(self, session_data: dict, single: bool = False) -> None: 

42 """ 

43 Save a session. If single=True, write as a dict (overwrite file). If False (default), append to list (archive mode). 

44 """ 

45 with FileLock(self.lock_path): 

46 if single: 

47 self._write_json(session_data) 

48 else: 

49 sessions = self.load_sessions(lock=False) 

50 sessions.append(session_data) 

51 self._write_json(sessions) 

52 

53 def save_single_session(self, session_data: dict) -> None: 

54 """ 

55 Save a single session as a dict (overwrite file). For plugin recap output. 

56 """ 

57 self.save_session(session_data, single=True) 

58 

59 def load_sessions(self, lock: bool = True) -> List[dict]: 

60 if lock: 

61 with FileLock(self.lock_path): 

62 return self._load_sessions_unlocked() 

63 else: 

64 return self._load_sessions_unlocked() 

65 

66 def _load_sessions_unlocked(self) -> List[dict]: 

67 try: 

68 with open(self.file_path, "r", encoding="utf-8") as f: 

69 data = json.load(f) 

70 return data if isinstance(data, list) else data.get("sessions", []) 

71 except (FileNotFoundError, json.JSONDecodeError): 

72 return [] 

73 

74 def _write_json(self, data) -> None: 

75 with open(self.file_path, "w", encoding="utf-8") as f: 

76 json.dump(data, f, indent=2)