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
« 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.
17"""Integration test for database unavailability."""
19import os
20import shutil
21import subprocess
22import tempfile
23import unittest
24from unittest.mock import patch
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)
37from oc_meta.run.meta_process import MetaProcess
39BASE_DIR = os.path.join("test", "meta_process")
40SHORT_TIMEOUT = 3
41SHORT_MAX_RETRIES = 1
42SHORT_BACKOFF = 0.1
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)
53class TestDatabaseUnavailability(unittest.TestCase):
54 """Test that database unavailability is handled correctly."""
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")
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")
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")
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)
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 })
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")
103 filename = files_to_process[0]
104 print(f"[DEBUG] test: file to process: {filename}")
106 print("[DEBUG] test: stopping Virtuoso...")
107 subprocess.run(["docker", "stop", VIRTUOSO_CONTAINER], capture_output=True, check=True)
108 print("[DEBUG] test: Virtuoso stopped")
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}")
122 self.assertNotEqual(result[0]["message"], "success", "Should fail when database unavailable")
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")
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")
135if __name__ == "__main__":
136 unittest.main()