|
27 | 27 | CONF = cfg.CONF |
28 | 28 |
|
29 | 29 |
|
| 30 | +@pytest.fixture |
| 31 | +def extract_dates(): |
| 32 | + """Fixture for extraction date range.""" |
| 33 | + return { |
| 34 | + "extract_from": datetime.datetime(2023, 5, 25, 0, 0, 0), |
| 35 | + "extract_to": datetime.datetime(2023, 5, 25, 23, 59, 59), |
| 36 | + } |
| 37 | + |
| 38 | + |
| 39 | +@pytest.fixture |
| 40 | +def mock_server(): |
| 41 | + """Fixture for a mock server.""" |
| 42 | + server = mock.Mock() |
| 43 | + server.id = "e3c5aeef-37b8-4332-ad9f-9d068f156dc2" |
| 44 | + server.name = "test-vm-1" |
| 45 | + server.status = "ACTIVE" |
| 46 | + server.created = "2023-05-25T12:00:00Z" |
| 47 | + server.flavor = {"id": "flavor-1"} |
| 48 | + return server |
| 49 | + |
| 50 | + |
| 51 | +@pytest.fixture |
| 52 | +def mock_flavors(): |
| 53 | + """Fixture for mock flavors.""" |
| 54 | + return {"flavor-1": {"vcpus": 2, "id": "flavor-1"}} |
| 55 | + |
| 56 | + |
| 57 | +@pytest.fixture |
| 58 | +def configured_extractor(mock_flavors): |
| 59 | + """Fixture for a configured EnergyConsumptionExtractor.""" |
| 60 | + # Configure CONF |
| 61 | + CONF.set_override("site_name", "TEST-Site") |
| 62 | + CONF.set_override("service_name", "TEST-Service") |
| 63 | + |
| 64 | + with mock.patch( |
| 65 | + "caso.extract.openstack.base.BaseOpenStackExtractor.__init__", |
| 66 | + return_value=None, |
| 67 | + ), mock.patch( |
| 68 | + "caso.extract.prometheus.EnergyConsumptionExtractor._get_flavors", |
| 69 | + return_value=mock_flavors, |
| 70 | + ), mock.patch( |
| 71 | + "caso.extract.openstack.base.BaseOpenStackExtractor._get_nova_client" |
| 72 | + ): |
| 73 | + extractor = EnergyConsumptionExtractor("test-project", "test-vo") |
| 74 | + extractor.project = "test-project" |
| 75 | + extractor.vo = "test-vo" |
| 76 | + extractor.project_id = "test-project-id" |
| 77 | + extractor.cloud_type = "openstack" |
| 78 | + yield extractor |
| 79 | + |
| 80 | + |
| 81 | +@pytest.fixture |
| 82 | +def prometheus_success_response(): |
| 83 | + """Fixture for a successful Prometheus response.""" |
| 84 | + response = mock.Mock() |
| 85 | + response.json.return_value = { |
| 86 | + "status": "success", |
| 87 | + "data": { |
| 88 | + "result": [ |
| 89 | + { |
| 90 | + "metric": {"instance": "test"}, |
| 91 | + "value": [1685051946, "5.0"], |
| 92 | + } |
| 93 | + ] |
| 94 | + }, |
| 95 | + } |
| 96 | + response.raise_for_status = mock.Mock() |
| 97 | + return response |
| 98 | + |
| 99 | + |
| 100 | +@pytest.fixture |
| 101 | +def prometheus_error_response(): |
| 102 | + """Fixture for a failed Prometheus response.""" |
| 103 | + response = mock.Mock() |
| 104 | + response.json.return_value = { |
| 105 | + "status": "error", |
| 106 | + "error": "query failed", |
| 107 | + } |
| 108 | + response.raise_for_status = mock.Mock() |
| 109 | + return response |
| 110 | + |
| 111 | + |
30 | 112 | class TestEnergyConsumptionExtractor: |
31 | 113 | """Test the energy consumption extractor.""" |
32 | 114 |
|
33 | | - @mock.patch("caso.extract.prometheus.EnergyConsumptionExtractor._get_flavors") |
34 | | - @mock.patch("caso.extract.openstack.base.BaseOpenStackExtractor._get_nova_client") |
35 | | - @mock.patch("caso.extract.openstack.base.BaseOpenStackExtractor.__init__") |
36 | 115 | @mock.patch("caso.extract.prometheus.requests.get") |
37 | 116 | def test_extract_with_results( |
38 | | - self, mock_get, mock_base_init, mock_get_nova, mock_get_flavors |
| 117 | + self, |
| 118 | + mock_get, |
| 119 | + configured_extractor, |
| 120 | + mock_server, |
| 121 | + extract_dates, |
| 122 | + prometheus_success_response, |
39 | 123 | ): |
40 | 124 | """Test extraction with successful Prometheus query.""" |
41 | | - # Configure CONF |
42 | | - CONF.set_override("site_name", "TEST-Site") |
43 | | - CONF.set_override("service_name", "TEST-Service") |
44 | | - |
45 | | - # Mock the base class __init__ to do nothing |
46 | | - mock_base_init.return_value = None |
47 | | - |
48 | | - # Mock flavors |
49 | | - mock_get_flavors.return_value = {"flavor-1": {"vcpus": 2, "id": "flavor-1"}} |
50 | | - |
51 | | - # Mock Nova client and servers |
52 | | - mock_server1 = mock.Mock() |
53 | | - mock_server1.id = "e3c5aeef-37b8-4332-ad9f-9d068f156dc2" |
54 | | - mock_server1.name = "test-vm-1" |
55 | | - mock_server1.status = "ACTIVE" |
56 | | - mock_server1.created = "2023-05-25T12:00:00Z" |
57 | | - mock_server1.flavor = {"id": "flavor-1"} |
58 | | - |
| 125 | + # Create a second server |
59 | 126 | mock_server2 = mock.Mock() |
60 | 127 | mock_server2.id = "f4d6bedf-48c9-5f2f-b043-ebb4f9e65d73" |
61 | 128 | mock_server2.name = "test-vm-2" |
62 | 129 | mock_server2.status = "ACTIVE" |
63 | 130 | mock_server2.created = "2023-05-25T12:00:00Z" |
64 | 131 | mock_server2.flavor = {"id": "flavor-1"} |
65 | 132 |
|
66 | | - mock_nova = mock.Mock() |
67 | | - mock_nova.servers.list.return_value = [mock_server1, mock_server2] |
68 | | - mock_get_nova.return_value = mock_nova |
69 | | - |
70 | | - # Create extractor and manually set required attributes |
71 | | - extractor = EnergyConsumptionExtractor("test-project", "test-vo") |
72 | | - extractor.project = "test-project" |
73 | | - extractor.vo = "test-vo" |
74 | | - extractor.project_id = "test-project-id" |
75 | | - extractor.cloud_type = "openstack" |
| 133 | + # Mock Nova client with servers |
| 134 | + configured_extractor.nova = mock.Mock() |
| 135 | + configured_extractor.nova.servers.list.return_value = [ |
| 136 | + mock_server, |
| 137 | + mock_server2, |
| 138 | + ] |
76 | 139 |
|
77 | 140 | # Mock Prometheus response |
78 | | - mock_response = mock.Mock() |
79 | | - mock_response.json.return_value = { |
80 | | - "status": "success", |
81 | | - "data": { |
82 | | - "result": [ |
83 | | - { |
84 | | - "metric": {"instance": "test"}, |
85 | | - "value": [1685051946, "5.0"], |
86 | | - } |
87 | | - ] |
88 | | - }, |
89 | | - } |
90 | | - mock_response.raise_for_status = mock.Mock() |
91 | | - mock_get.return_value = mock_response |
| 141 | + mock_get.return_value = prometheus_success_response |
92 | 142 |
|
93 | 143 | # Extract records |
94 | | - extract_from = datetime.datetime(2023, 5, 25, 0, 0, 0) |
95 | | - extract_to = datetime.datetime(2023, 5, 25, 23, 59, 59) |
96 | | - records = extractor.extract(extract_from, extract_to) |
| 144 | + records = configured_extractor.extract(**extract_dates) |
97 | 145 |
|
98 | 146 | # Verify - should create 2 records (one per VM) |
99 | 147 | assert len(records) == 2 |
100 | 148 | assert records[0].energy_wh == 5.0 |
101 | 149 | assert records[0].owner == "test-vo" |
102 | 150 | assert records[0].status == "active" |
103 | 151 |
|
104 | | - @mock.patch("caso.extract.prometheus.EnergyConsumptionExtractor._get_flavors") |
105 | | - @mock.patch("caso.extract.openstack.base.BaseOpenStackExtractor._get_nova_client") |
106 | | - @mock.patch("caso.extract.openstack.base.BaseOpenStackExtractor.__init__") |
107 | | - def test_extract_with_no_vms(self, mock_base_init, mock_get_nova, mock_get_flavors): |
| 152 | + def test_extract_with_no_vms(self, configured_extractor, extract_dates): |
108 | 153 | """Test extraction when there are no VMs.""" |
109 | | - # Configure CONF |
110 | | - CONF.set_override("site_name", "TEST-Site") |
111 | | - CONF.set_override("service_name", "TEST-Service") |
112 | | - |
113 | | - # Mock the base class __init__ to do nothing |
114 | | - mock_base_init.return_value = None |
115 | | - mock_get_flavors.return_value = {} |
116 | | - |
117 | 154 | # Mock Nova client with no servers |
118 | | - mock_nova = mock.Mock() |
119 | | - mock_nova.servers.list.return_value = [] |
120 | | - mock_get_nova.return_value = mock_nova |
121 | | - |
122 | | - # Create extractor and manually set required attributes |
123 | | - extractor = EnergyConsumptionExtractor("test-project", "test-vo") |
124 | | - extractor.project = "test-project" |
125 | | - extractor.vo = "test-vo" |
126 | | - extractor.project_id = "test-project-id" |
| 155 | + configured_extractor.nova = mock.Mock() |
| 156 | + configured_extractor.nova.servers.list.return_value = [] |
127 | 157 |
|
128 | 158 | # Extract records |
129 | | - extract_from = datetime.datetime(2023, 5, 25, 0, 0, 0) |
130 | | - extract_to = datetime.datetime(2023, 5, 25, 23, 59, 59) |
131 | | - records = extractor.extract(extract_from, extract_to) |
| 159 | + records = configured_extractor.extract(**extract_dates) |
132 | 160 |
|
133 | 161 | # Verify - no VMs, no records |
134 | 162 | assert len(records) == 0 |
135 | 163 |
|
136 | | - @mock.patch("caso.extract.prometheus.EnergyConsumptionExtractor._get_flavors") |
137 | | - @mock.patch("caso.extract.openstack.base.BaseOpenStackExtractor._get_nova_client") |
138 | | - @mock.patch("caso.extract.openstack.base.BaseOpenStackExtractor.__init__") |
139 | 164 | @mock.patch("caso.extract.prometheus.requests.get") |
140 | 165 | def test_extract_with_failed_query( |
141 | | - self, mock_get, mock_base_init, mock_get_nova, mock_get_flavors |
| 166 | + self, |
| 167 | + mock_get, |
| 168 | + configured_extractor, |
| 169 | + mock_server, |
| 170 | + extract_dates, |
| 171 | + prometheus_error_response, |
142 | 172 | ): |
143 | 173 | """Test extraction when Prometheus query fails.""" |
144 | | - # Configure CONF |
145 | | - CONF.set_override("site_name", "TEST-Site") |
146 | | - CONF.set_override("service_name", "TEST-Service") |
147 | | - |
148 | | - # Mock the base class __init__ to do nothing |
149 | | - mock_base_init.return_value = None |
150 | | - mock_get_flavors.return_value = {} |
151 | | - |
152 | | - # Mock Nova client and servers |
153 | | - mock_server = mock.Mock() |
154 | | - mock_server.id = "vm-uuid-1" |
155 | | - mock_server.name = "test-vm-1" |
156 | | - mock_server.status = "ACTIVE" |
157 | | - mock_server.created = "2023-05-25T12:00:00Z" |
158 | | - mock_server.flavor = {"id": "flavor-1"} |
159 | | - |
160 | | - mock_nova = mock.Mock() |
161 | | - mock_nova.servers.list.return_value = [mock_server] |
162 | | - mock_get_nova.return_value = mock_nova |
163 | | - |
164 | | - # Create extractor and manually set required attributes |
165 | | - extractor = EnergyConsumptionExtractor("test-project", "test-vo") |
166 | | - extractor.project = "test-project" |
167 | | - extractor.vo = "test-vo" |
168 | | - extractor.project_id = "test-project-id" |
169 | | - extractor.cloud_type = "openstack" |
| 174 | + # Mock Nova client with servers |
| 175 | + configured_extractor.nova = mock.Mock() |
| 176 | + configured_extractor.nova.servers.list.return_value = [mock_server] |
170 | 177 |
|
171 | 178 | # Mock Prometheus error response |
172 | | - mock_response = mock.Mock() |
173 | | - mock_response.json.return_value = { |
174 | | - "status": "error", |
175 | | - "error": "query failed", |
176 | | - } |
177 | | - mock_response.raise_for_status = mock.Mock() |
178 | | - mock_get.return_value = mock_response |
| 179 | + mock_get.return_value = prometheus_error_response |
179 | 180 |
|
180 | 181 | # Extract records |
181 | | - extract_from = datetime.datetime(2023, 5, 25, 0, 0, 0) |
182 | | - extract_to = datetime.datetime(2023, 5, 25, 23, 59, 59) |
183 | | - records = extractor.extract(extract_from, extract_to) |
| 182 | + records = configured_extractor.extract(**extract_dates) |
184 | 183 |
|
185 | 184 | # Verify - query failed, no records |
186 | 185 | assert len(records) == 0 |
187 | 186 |
|
188 | | - @mock.patch("caso.extract.prometheus.EnergyConsumptionExtractor._get_flavors") |
189 | | - @mock.patch("caso.extract.openstack.base.BaseOpenStackExtractor._get_nova_client") |
190 | | - @mock.patch("caso.extract.openstack.base.BaseOpenStackExtractor.__init__") |
191 | | - @mock.patch("caso.extract.prometheus.requests.get") |
192 | 187 | @mock.patch("caso.extract.prometheus.LOG") |
| 188 | + @mock.patch("caso.extract.prometheus.requests.get") |
193 | 189 | def test_extract_with_request_exception( |
194 | | - self, mock_log, mock_get, mock_base_init, mock_get_nova, mock_get_flavors |
| 190 | + self, mock_get, mock_log, configured_extractor, mock_server, extract_dates |
195 | 191 | ): |
196 | 192 | """Test extraction when request to Prometheus fails.""" |
197 | | - # Configure CONF |
198 | | - CONF.set_override("site_name", "TEST-Site") |
199 | | - CONF.set_override("service_name", "TEST-Service") |
200 | | - |
201 | | - # Mock the base class __init__ to do nothing |
202 | | - mock_base_init.return_value = None |
203 | | - mock_get_flavors.return_value = {} |
204 | | - |
205 | | - # Mock Nova client and servers |
206 | | - mock_server = mock.Mock() |
207 | | - mock_server.id = "vm-uuid-1" |
208 | | - mock_server.name = "test-vm-1" |
209 | | - mock_server.status = "ACTIVE" |
210 | | - mock_server.created = "2023-05-25T12:00:00Z" |
211 | | - mock_server.flavor = {"id": "flavor-1"} |
212 | | - |
213 | | - mock_nova = mock.Mock() |
214 | | - mock_nova.servers.list.return_value = [mock_server] |
215 | | - mock_get_nova.return_value = mock_nova |
216 | | - |
217 | | - # Create extractor and manually set required attributes |
218 | | - extractor = EnergyConsumptionExtractor("test-project", "test-vo") |
219 | | - extractor.project = "test-project" |
220 | | - extractor.vo = "test-vo" |
221 | | - extractor.project_id = "test-project-id" |
222 | | - extractor.cloud_type = "openstack" |
| 193 | + # Mock Nova client with servers |
| 194 | + configured_extractor.nova = mock.Mock() |
| 195 | + configured_extractor.nova.servers.list.return_value = [mock_server] |
223 | 196 |
|
224 | 197 | # Mock request exception |
225 | 198 | mock_get.side_effect = Exception("Connection error") |
226 | 199 |
|
227 | 200 | # Extract records |
228 | | - extract_from = datetime.datetime(2023, 5, 25, 0, 0, 0) |
229 | | - extract_to = datetime.datetime(2023, 5, 25, 23, 59, 59) |
230 | | - records = extractor.extract(extract_from, extract_to) |
| 201 | + records = configured_extractor.extract(**extract_dates) |
231 | 202 |
|
232 | 203 | # Verify - exception caught, no records |
233 | 204 | assert len(records) == 0 |
|
0 commit comments