Coverage for test/database_unavailability_test.py: 95%

75 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2025-12-20 08:55 +0000

1#!/usr/bin/python 

2# -*- coding: utf-8 -*- 

3# Copyright (c) 2022-2025 OpenCitations 

4# 

5# Permission to use, copy, modify, and/or distribute this software for any purpose 

6# with or without fee is hereby granted, provided that the above copyright notice 

7# and this permission notice appear in all copies. 

8# 

9# THE SOFTWARE IS PROVIDED 'AS IS' AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 

10# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 

11# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, 

12# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, 

13# DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 

14# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 

15# SOFTWARE. 

16 

17"""Integration test for database unavailability.""" 

18 

19import os 

20import shutil 

21import subprocess 

22import tempfile 

23import unittest 

24from unittest.mock import patch 

25 

26import yaml 

27from sparqlite import SPARQLClient as OriginalSPARQLClient 

28from test.test_utils import ( 

29 REDIS_CACHE_DB, 

30 SERVER, 

31 VIRTUOSO_CONTAINER, 

32 reset_redis_counters, 

33 reset_server, 

34 wait_for_virtuoso, 

35) 

36 

37from oc_meta.run.meta_process import MetaProcess 

38 

39BASE_DIR = os.path.join("test", "meta_process") 

40SHORT_TIMEOUT = 3 

41SHORT_MAX_RETRIES = 1 

42SHORT_BACKOFF = 0.1 

43 

44 

45def short_timeout_sparql_client(*args, **kwargs): 

46 """Wrapper that forces short timeout and minimal retries for SPARQLClient.""" 

47 kwargs["timeout"] = SHORT_TIMEOUT 

48 kwargs["max_retries"] = SHORT_MAX_RETRIES 

49 kwargs["backoff_factor"] = SHORT_BACKOFF 

50 return OriginalSPARQLClient(*args, **kwargs) 

51 

52 

53class TestDatabaseUnavailability(unittest.TestCase): 

54 """Test that database unavailability is handled correctly.""" 

55 

56 @classmethod 

57 def setUpClass(cls): 

58 print("[DEBUG] setUpClass: waiting for Virtuoso...") 

59 if not wait_for_virtuoso(SERVER, max_wait=30): 

60 raise TimeoutError("Virtuoso not ready") 

61 print("[DEBUG] setUpClass: Virtuoso ready") 

62 

63 def setUp(self): 

64 print("[DEBUG] setUp: creating temp dir...") 

65 self.temp_dir = tempfile.mkdtemp() 

66 print("[DEBUG] setUp: resetting server...") 

67 reset_server() 

68 print("[DEBUG] setUp: resetting redis...") 

69 reset_redis_counters() 

70 print("[DEBUG] setUp: done") 

71 

72 def tearDown(self): 

73 print("[DEBUG] tearDown: start") 

74 reset_redis_counters() 

75 if hasattr(self, "temp_dir") and os.path.exists(self.temp_dir): 

76 shutil.rmtree(self.temp_dir) 

77 subprocess.run(["docker", "start", VIRTUOSO_CONTAINER], capture_output=True, check=False) 

78 wait_for_virtuoso(SERVER, max_wait=30) 

79 print("[DEBUG] tearDown: done") 

80 

81 @patch("oc_meta.lib.finder.SPARQLClient", side_effect=short_timeout_sparql_client) 

82 @patch("sparqlite.SPARQLClient", side_effect=short_timeout_sparql_client) 

83 def test_virtuoso_unavailable_prevents_cache_update(self, mock_sparqlite, mock_finder): 

84 """When Virtuoso is offline, processing fails and file is NOT cached.""" 

85 print("[DEBUG] test: loading config...") 

86 meta_config_path = os.path.join(BASE_DIR, "meta_config_3.yaml") 

87 with open(meta_config_path, encoding="utf-8") as f: 

88 settings = yaml.full_load(f) 

89 

90 settings.update({ 

91 "redis_cache_db": REDIS_CACHE_DB, 

92 "ts_upload_cache": os.path.join(self.temp_dir, "cache.json"), 

93 "ts_failed_queries": os.path.join(self.temp_dir, "failed.txt"), 

94 "ts_stop_file": os.path.join(self.temp_dir, ".stop"), 

95 }) 

96 

97 print("[DEBUG] test: creating MetaProcess...") 

98 meta_process = MetaProcess(settings=settings, meta_config_path=meta_config_path) 

99 print("[DEBUG] test: preparing folders...") 

100 files_to_process = meta_process.prepare_folders() 

101 self.assertGreater(len(files_to_process), 0, "No input files found") 

102 

103 filename = files_to_process[0] 

104 print(f"[DEBUG] test: file to process: {filename}") 

105 

106 print("[DEBUG] test: stopping Virtuoso...") 

107 subprocess.run(["docker", "stop", VIRTUOSO_CONTAINER], capture_output=True, check=True) 

108 print("[DEBUG] test: Virtuoso stopped") 

109 

110 try: 

111 print("[DEBUG] test: calling curate_and_create...") 

112 result = meta_process.curate_and_create( 

113 filename, 

114 meta_process.cache_path, 

115 meta_process.errors_path, 

116 resp_agents_only=False, 

117 settings=settings, 

118 meta_config_path=meta_config_path, 

119 ) 

120 print(f"[DEBUG] test: result = {result}") 

121 

122 self.assertNotEqual(result[0]["message"], "success", "Should fail when database unavailable") 

123 

124 if os.path.exists(meta_process.cache_path): 

125 with open(meta_process.cache_path) as f: 

126 self.assertNotIn(filename, f.read(), "File should NOT be cached when upload fails") 

127 

128 finally: 

129 print("[DEBUG] test: restarting Virtuoso...") 

130 subprocess.run(["docker", "start", VIRTUOSO_CONTAINER], capture_output=True, check=True) 

131 wait_for_virtuoso(SERVER, max_wait=30) 

132 print("[DEBUG] test: done") 

133 

134 

135if __name__ == "__main__": 

136 unittest.main()