crisp-ase 1.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CRISP/__init__.py +99 -0
- CRISP/_version.py +1 -0
- CRISP/cli.py +41 -0
- CRISP/data_analysis/__init__.py +38 -0
- CRISP/data_analysis/clustering.py +838 -0
- CRISP/data_analysis/contact_coordination.py +915 -0
- CRISP/data_analysis/h_bond.py +772 -0
- CRISP/data_analysis/msd.py +1199 -0
- CRISP/data_analysis/prdf.py +404 -0
- CRISP/data_analysis/volumetric_atomic_density.py +527 -0
- CRISP/py.typed +1 -0
- CRISP/simulation_utility/__init__.py +31 -0
- CRISP/simulation_utility/atomic_indices.py +155 -0
- CRISP/simulation_utility/atomic_traj_linemap.py +278 -0
- CRISP/simulation_utility/error_analysis.py +254 -0
- CRISP/simulation_utility/interatomic_distances.py +200 -0
- CRISP/simulation_utility/subsampling.py +241 -0
- CRISP/tests/DataAnalysis/__init__.py +1 -0
- CRISP/tests/DataAnalysis/test_clustering_extended.py +212 -0
- CRISP/tests/DataAnalysis/test_contact_coordination.py +184 -0
- CRISP/tests/DataAnalysis/test_contact_coordination_extended.py +465 -0
- CRISP/tests/DataAnalysis/test_h_bond_complete.py +326 -0
- CRISP/tests/DataAnalysis/test_h_bond_extended.py +322 -0
- CRISP/tests/DataAnalysis/test_msd_complete.py +305 -0
- CRISP/tests/DataAnalysis/test_msd_extended.py +522 -0
- CRISP/tests/DataAnalysis/test_prdf.py +206 -0
- CRISP/tests/DataAnalysis/test_volumetric_atomic_density.py +463 -0
- CRISP/tests/SimulationUtility/__init__.py +1 -0
- CRISP/tests/SimulationUtility/test_atomic_traj_linemap.py +101 -0
- CRISP/tests/SimulationUtility/test_atomic_traj_linemap_extended.py +469 -0
- CRISP/tests/SimulationUtility/test_error_analysis_extended.py +151 -0
- CRISP/tests/SimulationUtility/test_interatomic_distances.py +223 -0
- CRISP/tests/SimulationUtility/test_subsampling.py +365 -0
- CRISP/tests/__init__.py +1 -0
- CRISP/tests/test_CRISP.py +28 -0
- CRISP/tests/test_cli.py +87 -0
- CRISP/tests/test_crisp_comprehensive.py +679 -0
- crisp_ase-1.1.2.dist-info/METADATA +141 -0
- crisp_ase-1.1.2.dist-info/RECORD +42 -0
- crisp_ase-1.1.2.dist-info/WHEEL +5 -0
- crisp_ase-1.1.2.dist-info/entry_points.txt +2 -0
- crisp_ase-1.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Extended tests for subsampling module."""
|
|
2
|
+
import pytest
|
|
3
|
+
import numpy as np
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import shutil
|
|
7
|
+
from ase import Atoms
|
|
8
|
+
from ase.io import write
|
|
9
|
+
|
|
10
|
+
from CRISP.simulation_utility.subsampling import (
|
|
11
|
+
indices,
|
|
12
|
+
compute_soap,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestSubsamplingIndicesFunction:
|
|
17
|
+
"""Test indices selection function."""
|
|
18
|
+
|
|
19
|
+
def test_indices_all_atoms(self):
|
|
20
|
+
"""Test selecting all atoms."""
|
|
21
|
+
atoms = Atoms('H2O', positions=[
|
|
22
|
+
[0.0, 0.0, 0.0],
|
|
23
|
+
[0.96, 0.0, 0.0],
|
|
24
|
+
[0.24, 0.93, 0.0]
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
result = indices(atoms, "all")
|
|
28
|
+
assert np.array_equal(result, np.array([0, 1, 2]))
|
|
29
|
+
|
|
30
|
+
def test_indices_none_defaults_to_all(self):
|
|
31
|
+
"""Test that None defaults to all atoms."""
|
|
32
|
+
atoms = Atoms('H2O', positions=[
|
|
33
|
+
[0.0, 0.0, 0.0],
|
|
34
|
+
[0.96, 0.0, 0.0],
|
|
35
|
+
[0.24, 0.93, 0.0]
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
result = indices(atoms, None)
|
|
39
|
+
assert np.array_equal(result, np.array([0, 1, 2]))
|
|
40
|
+
|
|
41
|
+
def test_indices_with_list_integers(self):
|
|
42
|
+
"""Test selecting atoms by integer indices."""
|
|
43
|
+
atoms = Atoms('H2O', positions=[
|
|
44
|
+
[0.0, 0.0, 0.0],
|
|
45
|
+
[0.96, 0.0, 0.0],
|
|
46
|
+
[0.24, 0.93, 0.0]
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
result = indices(atoms, [0, 2])
|
|
50
|
+
assert np.array_equal(result, np.array([0, 2]))
|
|
51
|
+
|
|
52
|
+
def test_indices_with_single_integer(self):
|
|
53
|
+
"""Test selecting single atom by integer."""
|
|
54
|
+
atoms = Atoms('H2O', positions=[
|
|
55
|
+
[0.0, 0.0, 0.0],
|
|
56
|
+
[0.96, 0.0, 0.0],
|
|
57
|
+
[0.24, 0.93, 0.0]
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
result = indices(atoms, [1])
|
|
61
|
+
assert np.array_equal(result, np.array([1]))
|
|
62
|
+
|
|
63
|
+
def test_indices_with_chemical_symbols(self):
|
|
64
|
+
"""Test selecting atoms by chemical symbol."""
|
|
65
|
+
atoms = Atoms('H2O', positions=[
|
|
66
|
+
[0.0, 0.0, 0.0],
|
|
67
|
+
[0.96, 0.0, 0.0],
|
|
68
|
+
[0.24, 0.93, 0.0]
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
result = indices(atoms, ['H'])
|
|
72
|
+
# Water has H atoms - verify we get at least 2 H atoms
|
|
73
|
+
assert len(result) == 2
|
|
74
|
+
assert all(isinstance(idx, (int, np.integer)) for idx in result)
|
|
75
|
+
|
|
76
|
+
def test_indices_with_oxygen_symbol(self):
|
|
77
|
+
"""Test selecting oxygen atoms from water."""
|
|
78
|
+
atoms = Atoms('H2O', positions=[
|
|
79
|
+
[0.0, 0.0, 0.0],
|
|
80
|
+
[0.96, 0.0, 0.0],
|
|
81
|
+
[0.24, 0.93, 0.0]
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
result = indices(atoms, ['O'])
|
|
85
|
+
# Water has exactly 1 oxygen atom
|
|
86
|
+
assert len(result) == 1
|
|
87
|
+
assert result[0] in [0, 1, 2] # Oxygen can be at any position depending on ASE ordering
|
|
88
|
+
|
|
89
|
+
def test_indices_with_multiple_symbols(self):
|
|
90
|
+
"""Test selecting multiple symbol types."""
|
|
91
|
+
atoms = Atoms('H2O', positions=[
|
|
92
|
+
[0.0, 0.0, 0.0],
|
|
93
|
+
[0.96, 0.0, 0.0],
|
|
94
|
+
[0.24, 0.93, 0.0]
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
result = indices(atoms, ['H', 'O'])
|
|
98
|
+
assert len(result) == 3
|
|
99
|
+
|
|
100
|
+
def test_indices_from_file(self):
|
|
101
|
+
"""Test loading indices from NumPy file."""
|
|
102
|
+
temp_dir = tempfile.mkdtemp()
|
|
103
|
+
try:
|
|
104
|
+
atoms = Atoms('H2O', positions=[
|
|
105
|
+
[0.0, 0.0, 0.0],
|
|
106
|
+
[0.96, 0.0, 0.0],
|
|
107
|
+
[0.24, 0.93, 0.0]
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
indices_file = os.path.join(temp_dir, 'indices.npy')
|
|
111
|
+
np.save(indices_file, np.array([0, 1]))
|
|
112
|
+
|
|
113
|
+
result = indices(atoms, indices_file)
|
|
114
|
+
assert np.array_equal(result, np.array([0, 1]))
|
|
115
|
+
finally:
|
|
116
|
+
shutil.rmtree(temp_dir)
|
|
117
|
+
|
|
118
|
+
def test_indices_invalid_type_raises_error(self):
|
|
119
|
+
"""Test that invalid index type raises ValueError."""
|
|
120
|
+
atoms = Atoms('H2O', positions=[
|
|
121
|
+
[0.0, 0.0, 0.0],
|
|
122
|
+
[0.96, 0.0, 0.0],
|
|
123
|
+
[0.24, 0.93, 0.0]
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
with pytest.raises(ValueError, match="Invalid index type"):
|
|
127
|
+
indices(atoms, [1.5]) # Float is invalid
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestComputeSoap:
|
|
131
|
+
"""Test SOAP descriptor computation."""
|
|
132
|
+
|
|
133
|
+
def test_compute_soap_basic(self):
|
|
134
|
+
"""Test basic SOAP descriptor calculation."""
|
|
135
|
+
atoms = Atoms('H2O', positions=[
|
|
136
|
+
[0.0, 0.0, 0.0],
|
|
137
|
+
[0.96, 0.0, 0.0],
|
|
138
|
+
[0.24, 0.93, 0.0]
|
|
139
|
+
])
|
|
140
|
+
atoms.set_cell([10, 10, 10])
|
|
141
|
+
atoms.set_pbc([True, True, True])
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
soap_descriptor = compute_soap(
|
|
145
|
+
atoms,
|
|
146
|
+
all_spec=['H', 'O'],
|
|
147
|
+
rcut=3.0,
|
|
148
|
+
idx=[0]
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
assert isinstance(soap_descriptor, np.ndarray)
|
|
152
|
+
assert len(soap_descriptor) > 0
|
|
153
|
+
except Exception:
|
|
154
|
+
pytest.skip("SOAP descriptor computation requires dscribe library")
|
|
155
|
+
|
|
156
|
+
def test_compute_soap_multiple_centers(self):
|
|
157
|
+
"""Test SOAP with multiple atomic centers."""
|
|
158
|
+
atoms = Atoms('H2O', positions=[
|
|
159
|
+
[0.0, 0.0, 0.0],
|
|
160
|
+
[0.96, 0.0, 0.0],
|
|
161
|
+
[0.24, 0.93, 0.0]
|
|
162
|
+
])
|
|
163
|
+
atoms.set_cell([10, 10, 10])
|
|
164
|
+
atoms.set_pbc([True, True, True])
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
soap_descriptor = compute_soap(
|
|
168
|
+
atoms,
|
|
169
|
+
all_spec=['H', 'O'],
|
|
170
|
+
rcut=3.0,
|
|
171
|
+
idx=[0, 1]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert isinstance(soap_descriptor, np.ndarray)
|
|
175
|
+
assert len(soap_descriptor) > 0
|
|
176
|
+
except Exception:
|
|
177
|
+
pytest.skip("SOAP descriptor computation requires dscribe library")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestSubsamplingEdgeCases:
|
|
181
|
+
"""Test edge cases."""
|
|
182
|
+
|
|
183
|
+
def test_single_atom_system(self):
|
|
184
|
+
"""Test with single atom."""
|
|
185
|
+
atoms = Atoms('H', positions=[[0.0, 0.0, 0.0]])
|
|
186
|
+
|
|
187
|
+
result = indices(atoms, "all")
|
|
188
|
+
assert np.array_equal(result, np.array([0]))
|
|
189
|
+
|
|
190
|
+
def test_large_system(self):
|
|
191
|
+
"""Test with larger system."""
|
|
192
|
+
# Create a system with many atoms
|
|
193
|
+
symbols = ['H', 'C', 'N', 'O'] * 25 # 100 atoms
|
|
194
|
+
positions = np.random.rand(100, 3) * 10
|
|
195
|
+
atoms = Atoms(symbols, positions=positions)
|
|
196
|
+
atoms.set_cell([15, 15, 15])
|
|
197
|
+
atoms.set_pbc([True, True, True])
|
|
198
|
+
|
|
199
|
+
result = indices(atoms, "all")
|
|
200
|
+
assert len(result) == 100
|
|
201
|
+
|
|
202
|
+
def test_indices_with_duplicates(self):
|
|
203
|
+
"""Test selecting with duplicate indices."""
|
|
204
|
+
atoms = Atoms('H2O', positions=[
|
|
205
|
+
[0.0, 0.0, 0.0],
|
|
206
|
+
[0.96, 0.0, 0.0],
|
|
207
|
+
[0.24, 0.93, 0.0]
|
|
208
|
+
])
|
|
209
|
+
|
|
210
|
+
result = indices(atoms, [0, 0, 1])
|
|
211
|
+
# Should contain duplicates as provided
|
|
212
|
+
assert 0 in result
|
|
213
|
+
assert 1 in result
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TestSubsamplingParameterVariations:
|
|
217
|
+
"""Test with various parameter combinations."""
|
|
218
|
+
|
|
219
|
+
@pytest.mark.parametrize("rcut", [2.0, 3.0, 4.0, 5.0])
|
|
220
|
+
def test_soap_different_rcut(self, rcut):
|
|
221
|
+
"""Test SOAP with different cutoff radii."""
|
|
222
|
+
atoms = Atoms('H2O', positions=[
|
|
223
|
+
[0.0, 0.0, 0.0],
|
|
224
|
+
[0.96, 0.0, 0.0],
|
|
225
|
+
[0.24, 0.93, 0.0]
|
|
226
|
+
])
|
|
227
|
+
atoms.set_cell([10, 10, 10])
|
|
228
|
+
atoms.set_pbc([True, True, True])
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
soap_descriptor = compute_soap(
|
|
232
|
+
atoms,
|
|
233
|
+
all_spec=['H', 'O'],
|
|
234
|
+
rcut=rcut,
|
|
235
|
+
idx=[0]
|
|
236
|
+
)
|
|
237
|
+
assert isinstance(soap_descriptor, np.ndarray)
|
|
238
|
+
except Exception:
|
|
239
|
+
pytest.skip("SOAP computation requires dscribe library")
|
|
240
|
+
|
|
241
|
+
@pytest.mark.parametrize("symbol_list", [['H'], ['O'], ['H', 'O']])
|
|
242
|
+
def test_indices_different_symbols(self, symbol_list):
|
|
243
|
+
"""Test indices selection with different symbol combinations."""
|
|
244
|
+
atoms = Atoms('H2O', positions=[
|
|
245
|
+
[0.0, 0.0, 0.0],
|
|
246
|
+
[0.96, 0.0, 0.0],
|
|
247
|
+
[0.24, 0.93, 0.0]
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
result = indices(atoms, symbol_list)
|
|
251
|
+
assert len(result) > 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class TestSubsamplingIntegration:
|
|
255
|
+
"""Integration tests for subsampling."""
|
|
256
|
+
|
|
257
|
+
def test_workflow_select_and_soap(self):
|
|
258
|
+
"""Test complete workflow of selecting and analyzing."""
|
|
259
|
+
atoms = Atoms('H2O', positions=[
|
|
260
|
+
[0.0, 0.0, 0.0],
|
|
261
|
+
[0.96, 0.0, 0.0],
|
|
262
|
+
[0.24, 0.93, 0.0]
|
|
263
|
+
])
|
|
264
|
+
atoms.set_cell([10, 10, 10])
|
|
265
|
+
atoms.set_pbc([True, True, True])
|
|
266
|
+
|
|
267
|
+
# Select oxygen atoms
|
|
268
|
+
idx = indices(atoms, ['O'])
|
|
269
|
+
assert len(idx) > 0
|
|
270
|
+
|
|
271
|
+
# Compute SOAP
|
|
272
|
+
try:
|
|
273
|
+
soap_descriptor = compute_soap(
|
|
274
|
+
atoms,
|
|
275
|
+
all_spec=['H', 'O'],
|
|
276
|
+
rcut=3.0,
|
|
277
|
+
idx=idx
|
|
278
|
+
)
|
|
279
|
+
assert isinstance(soap_descriptor, np.ndarray)
|
|
280
|
+
except Exception:
|
|
281
|
+
pytest.skip("SOAP computation requires dscribe library")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class TestSubsamplingNumericalProperties:
|
|
285
|
+
"""Test numerical properties of computed values."""
|
|
286
|
+
|
|
287
|
+
def test_soap_descriptor_shape(self):
|
|
288
|
+
"""Test that SOAP descriptor has consistent shape."""
|
|
289
|
+
atoms = Atoms('H2O', positions=[
|
|
290
|
+
[0.0, 0.0, 0.0],
|
|
291
|
+
[0.96, 0.0, 0.0],
|
|
292
|
+
[0.24, 0.93, 0.0]
|
|
293
|
+
])
|
|
294
|
+
atoms.set_cell([10, 10, 10])
|
|
295
|
+
atoms.set_pbc([True, True, True])
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
soap1 = compute_soap(atoms, ['H', 'O'], 3.0, [0])
|
|
299
|
+
soap2 = compute_soap(atoms, ['H', 'O'], 3.0, [1])
|
|
300
|
+
|
|
301
|
+
assert soap1.shape == soap2.shape
|
|
302
|
+
except Exception:
|
|
303
|
+
pytest.skip("SOAP computation requires dscribe library")
|
|
304
|
+
|
|
305
|
+
def test_soap_descriptor_finite_values(self):
|
|
306
|
+
"""Test that SOAP descriptor contains finite values."""
|
|
307
|
+
atoms = Atoms('H2O', positions=[
|
|
308
|
+
[0.0, 0.0, 0.0],
|
|
309
|
+
[0.96, 0.0, 0.0],
|
|
310
|
+
[0.24, 0.93, 0.0]
|
|
311
|
+
])
|
|
312
|
+
atoms.set_cell([10, 10, 10])
|
|
313
|
+
atoms.set_pbc([True, True, True])
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
soap_descriptor = compute_soap(atoms, ['H', 'O'], 3.0, [0])
|
|
317
|
+
|
|
318
|
+
assert np.all(np.isfinite(soap_descriptor))
|
|
319
|
+
assert not np.all(soap_descriptor == 0)
|
|
320
|
+
except Exception:
|
|
321
|
+
pytest.skip("SOAP computation requires dscribe library")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class TestSubsamplingSystemTypes:
|
|
325
|
+
"""Test with different system types."""
|
|
326
|
+
|
|
327
|
+
def test_periodic_system(self):
|
|
328
|
+
"""Test with periodic system."""
|
|
329
|
+
atoms = Atoms('H2O', positions=[
|
|
330
|
+
[0.0, 0.0, 0.0],
|
|
331
|
+
[0.96, 0.0, 0.0],
|
|
332
|
+
[0.24, 0.93, 0.0]
|
|
333
|
+
])
|
|
334
|
+
atoms.set_cell([10, 10, 10])
|
|
335
|
+
atoms.set_pbc([True, True, True])
|
|
336
|
+
|
|
337
|
+
result = indices(atoms, "all")
|
|
338
|
+
assert len(result) == 3
|
|
339
|
+
|
|
340
|
+
def test_non_periodic_system(self):
|
|
341
|
+
"""Test with non-periodic system."""
|
|
342
|
+
atoms = Atoms('H2O', positions=[
|
|
343
|
+
[0.0, 0.0, 0.0],
|
|
344
|
+
[0.96, 0.0, 0.0],
|
|
345
|
+
[0.24, 0.93, 0.0]
|
|
346
|
+
])
|
|
347
|
+
# No cell or PBC set
|
|
348
|
+
|
|
349
|
+
result = indices(atoms, "all")
|
|
350
|
+
assert len(result) == 3
|
|
351
|
+
|
|
352
|
+
def test_mixed_element_system(self):
|
|
353
|
+
"""Test with system containing many different elements."""
|
|
354
|
+
symbols = ['H', 'C', 'N', 'O', 'S', 'P']
|
|
355
|
+
positions = [[0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0], [4, 0, 0], [5, 0, 0]]
|
|
356
|
+
atoms = Atoms(symbols, positions=positions)
|
|
357
|
+
|
|
358
|
+
# Test selecting each element
|
|
359
|
+
for symbol in symbols:
|
|
360
|
+
result = indices(atoms, [symbol])
|
|
361
|
+
assert len(result) > 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == '__main__':
|
|
365
|
+
pytest.main([__file__, '-v'])
|
CRISP/tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CRISP tests package."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Basic CRISP package tests."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import CRISP
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_crisp_import():
|
|
8
|
+
"""Test that CRISP can be imported."""
|
|
9
|
+
assert CRISP is not None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_crisp_version():
|
|
13
|
+
"""Test that CRISP has a version."""
|
|
14
|
+
try:
|
|
15
|
+
version = CRISP.__version__
|
|
16
|
+
assert isinstance(version, str)
|
|
17
|
+
except AttributeError:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_crisp_modules():
|
|
22
|
+
"""Test that main modules can be imported."""
|
|
23
|
+
try:
|
|
24
|
+
from CRISP.simulation_utility import atomic_indices
|
|
25
|
+
from CRISP.data_analysis import msd
|
|
26
|
+
assert True
|
|
27
|
+
except ImportError as e:
|
|
28
|
+
pytest.fail(f"Failed to import CRISP modules: {e}")
|
CRISP/tests/test_cli.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Tests for CRISP command-line interface module."""
|
|
2
|
+
import pytest
|
|
3
|
+
import sys
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from CRISP.cli import main
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestCLIBasic:
|
|
10
|
+
"""Test basic CLI functionality."""
|
|
11
|
+
|
|
12
|
+
@patch('sys.argv', ['crisp', '--version'])
|
|
13
|
+
def test_version_flag(self):
|
|
14
|
+
"""Test --version flag displays version and exits."""
|
|
15
|
+
with patch('sys.stdout', new=StringIO()) as fake_out:
|
|
16
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
17
|
+
main()
|
|
18
|
+
assert exc_info.value.code == 0
|
|
19
|
+
|
|
20
|
+
@patch('sys.argv', ['crisp', '--help'])
|
|
21
|
+
def test_help_flag(self):
|
|
22
|
+
"""Test --help flag displays help and exits."""
|
|
23
|
+
with patch('sys.stdout', new=StringIO()) as fake_out:
|
|
24
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
25
|
+
main()
|
|
26
|
+
assert exc_info.value.code == 0
|
|
27
|
+
|
|
28
|
+
@patch('sys.argv', ['crisp'])
|
|
29
|
+
def test_no_arguments(self):
|
|
30
|
+
"""Test CLI with no arguments shows help."""
|
|
31
|
+
with patch('sys.stdout', new=StringIO()):
|
|
32
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
33
|
+
main()
|
|
34
|
+
assert exc_info.value.code == 1
|
|
35
|
+
|
|
36
|
+
@patch('sys.argv', ['crisp', 'test'])
|
|
37
|
+
@patch('CRISP.cli.run_tests')
|
|
38
|
+
def test_test_command_success(self, mock_run_tests):
|
|
39
|
+
"""Test 'test' subcommand when tests pass."""
|
|
40
|
+
# run_tests calls sys.exit, so main won't return
|
|
41
|
+
main()
|
|
42
|
+
mock_run_tests.assert_called_once_with(verbose=False)
|
|
43
|
+
|
|
44
|
+
@patch('sys.argv', ['crisp', 'test'])
|
|
45
|
+
@patch('CRISP.cli.run_tests')
|
|
46
|
+
def test_test_command_failure(self, mock_run_tests):
|
|
47
|
+
"""Test 'test' subcommand when tests fail."""
|
|
48
|
+
# run_tests calls sys.exit, so main won't return
|
|
49
|
+
main()
|
|
50
|
+
mock_run_tests.assert_called_once_with(verbose=False)
|
|
51
|
+
|
|
52
|
+
@patch('sys.argv', ['crisp', '--invalid-flag'])
|
|
53
|
+
def test_invalid_flag(self):
|
|
54
|
+
"""Test invalid command-line flag."""
|
|
55
|
+
with patch('sys.stderr', new=StringIO()):
|
|
56
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
57
|
+
main()
|
|
58
|
+
assert exc_info.value.code != 0
|
|
59
|
+
|
|
60
|
+
@patch('sys.argv', ['crisp', 'invalid_command'])
|
|
61
|
+
def test_invalid_command(self):
|
|
62
|
+
"""Test invalid subcommand."""
|
|
63
|
+
with patch('sys.stderr', new=StringIO()):
|
|
64
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
65
|
+
main()
|
|
66
|
+
assert exc_info.value.code != 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestCLIIntegration:
|
|
70
|
+
"""Test CLI integration."""
|
|
71
|
+
|
|
72
|
+
def test_cli_module_import(self):
|
|
73
|
+
"""Test CLI module can be imported."""
|
|
74
|
+
from CRISP import cli
|
|
75
|
+
assert hasattr(cli, 'main')
|
|
76
|
+
assert callable(cli.main)
|
|
77
|
+
|
|
78
|
+
def test_version_string_format(self):
|
|
79
|
+
"""Test version string is properly formatted."""
|
|
80
|
+
from CRISP._version import __version__
|
|
81
|
+
assert __version__
|
|
82
|
+
assert isinstance(__version__, str)
|
|
83
|
+
assert len(__version__) > 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == '__main__':
|
|
87
|
+
pytest.main([__file__, '-v'])
|