-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathtest_cli.py
More file actions
282 lines (237 loc) · 12.6 KB
/
Copy pathtest_cli.py
File metadata and controls
282 lines (237 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# tests/test_cli.py
import os
import subprocess
import json
import yaml
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
from pedalboard_pluginary.constants import APP_NAME, PLUGINS_CACHE_FILENAME
from pedalboard_pluginary.data import get_cache_path
# Helper to get the cache file path for plugins
def get_plugins_cache_file():
return get_cache_path(PLUGINS_CACHE_FILENAME)
@pytest.fixture(autouse=True)
def manage_plugin_cache():
"""Fixture to ensure plugin cache is handled before and after tests."""
cache_file = get_plugins_cache_file()
original_content = None
if cache_file.exists():
original_content = cache_file.read_text()
cache_file.unlink() # Remove before test
yield # Test runs here
# Cleanup: remove cache file created by test, or restore original
if cache_file.exists():
cache_file.unlink()
if original_content:
# Ensure parent directory exists before writing back
cache_file.parent.mkdir(parents=True, exist_ok=True)
cache_file.write_text(original_content)
# Mocked data for PedalboardScanner.scan_all_plugins and load_json_file
# This data will be "written" by the mocked scan and "read" by list/json/yaml
MOCK_PLUGIN_DATA = {
"vst3/MockSynth": {
"id": "vst3/MockSynth",
"name": "MockSynth",
"path": "/fake/path/to/MockSynth.vst3",
"filename": "MockSynth.vst3",
"plugin_type": "vst3",
"parameters": {
"Volume": {"name": "Volume", "value": 0.75},
"Pan": {"name": "Pan", "value": 0.0}
},
"manufacturer": "FakePlugins",
"name_in_file": "MockSynth"
},
"aufx/MockEffect": {
"id": "aufx/MockEffect",
"name": "MockEffect",
"path": "/fake/path/to/MockEffect.component",
"filename": "MockEffect.component",
"plugin_type": "aufx",
"parameters": {
"Wet/Dry": {"name": "Wet/Dry", "value": 0.5}
},
"manufacturer": "FakeAudio",
"name_in_file": "MockEffect"
}
}
# This mock will replace the actual PedalboardScanner instance or its methods
@patch('pedalboard_pluginary.scanner.PedalboardScanner.scan') # Patched to 'scan' as it's called by rescan
@patch('pedalboard_pluginary.scanner.PedalboardScanner.update') # Patched to 'update'
@patch('pedalboard_pluginary.scanner.PedalboardScanner.save_plugins') # Mock save_plugins
@patch('pedalboard_pluginary.core.PedalboardPluginary.load_data') # Mock load_data in core
def run_cli_command(
cli_args_list,
mock_core_load_data_mock, # Original mock object for core.PedalboardPluginary.load_data
mock_scanner_save_plugins_mock, # Original mock object for scanner.PedalboardScanner.save_plugins
mock_scanner_update_mock, # Original mock object for scanner.PedalboardScanner.update
mock_scanner_scan_mock, # Original mock object for scanner.PedalboardScanner.scan
expected_exit_code=0
):
"""Helper to run CLI commands and capture output."""
# If scan or update is called, make them "create" the mock cache file
def side_effect_scan_or_update(*args, **kwargs):
cache_file = get_plugins_cache_file()
cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(cache_file, 'w') as f:
json.dump(MOCK_PLUGIN_DATA, f, indent=4)
# The actual scan methods in PedalboardScanner don't return anything.
# They modify self.plugins and then call self.save_plugins.
# We've mocked save_plugins separately.
mock_scanner_scan_mock.side_effect = side_effect_scan_or_update
mock_scanner_update_mock.side_effect = side_effect_scan_or_update
# Mock load_data to set the plugins attribute on an instance if needed,
# or simply prevent it from trying to load a real file during list commands
# if scan hasn't run.
# For 'list', 'json', 'yaml', the PedalboardPluginary instance will try to load.
# We can patch load_json_file used by PedalboardPluginary.load_data
base_command = ["pbpluginary"]
full_command = base_command + cli_args_list
try:
result = subprocess.run(full_command, capture_output=True, text=True, check=False)
if result.returncode != expected_exit_code:
print("STDOUT:", result.stdout)
print("STDERR:", result.stderr)
assert result.returncode == expected_exit_code
return result
except FileNotFoundError:
pytest.fail("pbpluginary command not found. Ensure it's installed and in PATH for testing.")
# Test 'scan' command
# Patching at the source of where PedalboardScanner is instantiated or used by CLI
@patch('pedalboard_pluginary.__main__.PedalboardScanner')
def test_cli_scan(MockScannerConstructor):
# Mock the instance methods that would be called
mock_scanner_instance = MockScannerConstructor.return_value
mock_scanner_instance.rescan.return_value = None # rescan calls full_scan which calls scan_all_plugins
# We need rescan (which is an alias for full_scan) to effectively create the cache
# by having its underlying scan_all_plugins call write the MOCK_PLUGIN_DATA
def mock_rescan_writes_cache(*args, **kwargs):
cache_file = get_plugins_cache_file()
cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(cache_file, 'w') as f:
json.dump(MOCK_PLUGIN_DATA, f, indent=4)
mock_scanner_instance.rescan.side_effect = mock_rescan_writes_cache
result = run_cli_command(["scan"]) # Uses the patches from run_cli_command's decorators
# Check that the cache file was created with mock data
cache_file = get_plugins_cache_file()
assert cache_file.exists()
with open(cache_file, 'r') as f:
data_from_cache = json.load(f)
assert data_from_cache == MOCK_PLUGIN_DATA
# Check if scan method on the instance was called
mock_scanner_instance.rescan.assert_called_once()
# Test 'list' command (implicitly tests JSON output)
# For list, we need to ensure that the cache exists or that PedalboardPluginary can load it.
# The manage_plugin_cache fixture helps here.
# We also need to control what PedalboardPluginary.load_data does.
@patch('pedalboard_pluginary.data.load_json_file') # Patch where load_json_file is defined
def test_cli_list(mock_load_json, capsys):
# Setup: Ensure a cache file with MOCK_PLUGIN_DATA exists for 'list' to read
cache_file = get_plugins_cache_file()
cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(cache_file, 'w') as f:
json.dump(MOCK_PLUGIN_DATA, f, indent=4)
# Configure the mock for load_json_file used by PedalboardPluginary
# It should return the MOCK_PLUGIN_DATA when the specific plugins cache path is requested
def side_effect_load_json(path_arg):
if path_arg == cache_file:
# Return raw dict, PedalboardPluginary.load_data will handle reconstruction
return MOCK_PLUGIN_DATA
return {} # Default for other calls
mock_load_json.side_effect = side_effect_load_json
# Run the 'list' command
# Using direct function call to avoid subprocess complexity with stdout/stderr and fire's display hook
from pedalboard_pluginary.__main__ import list_json # Corrected to list_json
# Fire's Display hook is tricky to test with subprocess.run, so call directly.
# list_json returns a dict which fire then processes.
# We need to ensure that when `pbpluginary list` is run, this function is called
# and its output (which is JSON string) is printed.
# For simplicity here, just test the function that `fire` would call.
# To test the actual CLI output, we need to let pbpluginary run as subprocess
# and capture stdout. This means not mocking PedalboardPluginary or its load_data directly here
# but ensuring the underlying data.load_json_file behaves as expected due to the patch.
result = subprocess.run(["pbpluginary", "list"], capture_output=True, text=True, check=True)
# The output should be the MOCK_PLUGIN_DATA formatted as JSON
# Fire wraps output, so it might not be exact JSON string if printed line-by-line.
# The default 'list' command in __main__.py calls bdict().to_json() and fire prints it.
# Let's parse the stdout.
try:
output_data = json.loads(result.stdout)
assert output_data == MOCK_PLUGIN_DATA
except json.JSONDecodeError:
pytest.fail(f"CLI output was not valid JSON. Output:\n{result.stdout}")
# Test 'json' command (should be identical to 'list')
@patch('pedalboard_pluginary.data.load_json_file')
def test_cli_json_output(mock_load_json_for_json_cmd, capsys):
cache_file = get_plugins_cache_file()
cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(cache_file, 'w') as f:
json.dump(MOCK_PLUGIN_DATA, f, indent=4)
def side_effect_load_json(path_arg):
if path_arg == cache_file:
return MOCK_PLUGIN_DATA
return {}
mock_load_json_for_json_cmd.side_effect = side_effect_load_json
result = subprocess.run(["pbpluginary", "json"], capture_output=True, text=True, check=True)
try:
output_data = json.loads(result.stdout)
assert output_data == MOCK_PLUGIN_DATA
except json.JSONDecodeError:
pytest.fail(f"CLI output for 'json' was not valid JSON. Output:\n{result.stdout}")
# Test 'yaml' command
@patch('pedalboard_pluginary.data.load_json_file')
def test_cli_yaml_output(mock_load_json_for_yaml_cmd, capsys):
cache_file = get_plugins_cache_file()
cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(cache_file, 'w') as f:
json.dump(MOCK_PLUGIN_DATA, f, indent=4)
def side_effect_load_json(path_arg):
if path_arg == cache_file:
return MOCK_PLUGIN_DATA
return {}
mock_load_json_for_yaml_cmd.side_effect = side_effect_load_json
result = subprocess.run(["pbpluginary", "yaml"], capture_output=True, text=True, check=True)
try:
# python-benedict's to_yaml output might have specific formatting.
# For robustness, parse it back and compare with original data.
output_data = yaml.safe_load(result.stdout)
# YAML load might produce slightly different types (e.g. list for dict items sometimes)
# A direct comparison MOCK_PLUGIN_DATA might be tricky if numbers are float vs int.
# For now, let's assume benedict produces standard YAML that converts back cleanly.
assert json.dumps(output_data, sort_keys=True) == json.dumps(MOCK_PLUGIN_DATA, sort_keys=True)
except yaml.YAMLError:
pytest.fail(f"CLI output for 'yaml' was not valid YAML. Output:\n{result.stdout}")
except Exception as e:
pytest.fail(f"Error comparing YAML output: {e}. Output:\n{result.stdout}")
# Test 'update' command
@patch('pedalboard_pluginary.__main__.PedalboardScanner')
def test_cli_update(MockScannerConstructorForUpdate):
mock_scanner_instance = MockScannerConstructorForUpdate.return_value
# Simulate that update_scan effectively writes the cache
def mock_update_scan_writes_cache(*args, **kwargs):
cache_file = get_plugins_cache_file()
cache_file.parent.mkdir(parents=True, exist_ok=True)
# Update might add to existing data or rescan if no cache.
# For this test, assume it behaves like scan if no cache.
with open(cache_file, 'w') as f:
json.dump(MOCK_PLUGIN_DATA, f, indent=4) # For simplicity, same as scan
mock_scanner_instance.update.side_effect = mock_update_scan_writes_cache # Changed update_scan to update
result = run_cli_command(["update"]) # Uses patches from run_cli_command
cache_file = get_plugins_cache_file()
assert cache_file.exists()
with open(cache_file, 'r') as f:
data_from_cache = json.load(f)
assert data_from_cache == MOCK_PLUGIN_DATA # Assuming update wrote this
mock_scanner_instance.update.assert_called_once() # Changed update_scan to update
# TODO: Test for verbose logging options (--verbose=1, --verbose=2)
# TODO: Test for --extra-folders option with scan and update
# TODO: Test scan/update when cache already exists (for update's diff logic, though that's scanner internal)
# TODO: Test error conditions (e.g., unparseable cache, permissions issues - harder to mock)
# Note: The run_cli_command helper and its patches are quite broad.
# For more targeted tests, especially for 'list', 'json', 'yaml',
# it might be better to directly call the CLI functions from __main__.py
# and mock their dependencies (like PedalboardPluginary instance) instead of using subprocess.
# However, subprocess tests the actual command-line invocation.
# The current `test_cli_list` and `test_cli_json_output`, `test_cli_yaml_output`
# have been changed to use subprocess.run directly.