fastled 1.4.31__py3-none-any.whl → 1.4.32__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.
@@ -0,0 +1,373 @@
1
+ """Comprehensive test suite for FastLED install feature."""
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import sys
7
+ import tempfile
8
+ import unittest
9
+ from pathlib import Path
10
+ from unittest.mock import MagicMock, patch
11
+
12
+ # Add parent directory to path for imports
13
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
14
+
15
+ from fastled.install.main import auto_execute_fastled, fastled_install
16
+ from fastled.install.project_detection import (
17
+ check_existing_arduino_content,
18
+ )
19
+ from fastled.install.vscode_config import (
20
+ generate_fastled_tasks,
21
+ update_launch_json_for_arduino,
22
+ update_vscode_settings_for_fastled,
23
+ )
24
+
25
+
26
+ class TestFastLEDInstall(unittest.TestCase):
27
+ """Test FastLED installation functionality."""
28
+
29
+ def setUp(self):
30
+ """Set up test environment."""
31
+ self.test_dir = tempfile.mkdtemp()
32
+ self.original_cwd = os.getcwd()
33
+ os.chdir(self.test_dir)
34
+
35
+ def tearDown(self):
36
+ """Clean up test environment."""
37
+ os.chdir(self.original_cwd)
38
+ shutil.rmtree(self.test_dir)
39
+
40
+ @patch("fastled.install.project_detection.shutil.which")
41
+ @patch("builtins.input")
42
+ def test_dry_run_basic_project(self, mock_input, mock_which):
43
+ """Test 1: Dry-run in basic project."""
44
+ # Setup
45
+ mock_which.return_value = "/usr/bin/code"
46
+ mock_input.return_value = "y"
47
+ os.makedirs(".vscode")
48
+
49
+ # Run
50
+ result = fastled_install(dry_run=True, no_interactive=True)
51
+
52
+ # Verify
53
+ self.assertTrue(result)
54
+ self.assertTrue(Path(".vscode/launch.json").exists())
55
+ self.assertTrue(Path(".vscode/tasks.json").exists())
56
+
57
+ @patch("fastled.install.project_detection.is_fastled_repository")
58
+ @patch("fastled.install.project_detection.detect_fastled_project")
59
+ @patch("fastled.install.project_detection.shutil.which")
60
+ @patch("builtins.input")
61
+ def test_dry_run_fastled_external(
62
+ self, mock_input, mock_which, mock_detect, mock_repo
63
+ ):
64
+ """Test 2: Dry-run in external FastLED project."""
65
+ # Setup
66
+ mock_which.return_value = "/usr/bin/code"
67
+ mock_input.return_value = "y"
68
+ mock_detect.return_value = True
69
+ mock_repo.return_value = False
70
+ os.makedirs(".vscode")
71
+
72
+ # Create library.json
73
+ with open("library.json", "w") as f:
74
+ json.dump({"name": "FastLED"}, f)
75
+
76
+ # Run
77
+ result = fastled_install(dry_run=True, no_interactive=True)
78
+
79
+ # Verify
80
+ self.assertTrue(result)
81
+ self.assertFalse(Path(".vscode/settings.json").exists()) # No clangd settings
82
+
83
+ @patch("fastled.install.project_detection.is_fastled_repository")
84
+ @patch("fastled.install.project_detection.detect_fastled_project")
85
+ @patch("fastled.install.project_detection.shutil.which")
86
+ @patch("builtins.input")
87
+ def test_dry_run_fastled_repository(
88
+ self, mock_input, mock_which, mock_detect, mock_repo
89
+ ):
90
+ """Test 3: Dry-run in actual FastLED repository."""
91
+ # Setup
92
+ mock_which.return_value = "/usr/bin/code"
93
+ mock_input.return_value = "y"
94
+ mock_detect.return_value = True
95
+ mock_repo.return_value = True
96
+ os.makedirs(".vscode")
97
+
98
+ # Run
99
+ result = fastled_install(dry_run=True, no_interactive=True)
100
+
101
+ # Verify
102
+ self.assertTrue(result)
103
+ self.assertTrue(Path(".vscode/settings.json").exists()) # Has clangd settings
104
+
105
+ @patch("fastled.install.project_detection.shutil.which")
106
+ @patch("builtins.input")
107
+ def test_existing_vscode_project(self, mock_input, mock_which):
108
+ """Test 4: Merge with existing .vscode configs."""
109
+ # Setup
110
+ mock_which.return_value = "/usr/bin/code"
111
+ mock_input.return_value = "y"
112
+ os.makedirs(".vscode")
113
+
114
+ # Create existing launch.json
115
+ existing_config = {
116
+ "version": "0.2.0",
117
+ "configurations": [{"name": "Existing", "type": "node"}],
118
+ }
119
+ with open(".vscode/launch.json", "w") as f:
120
+ json.dump(existing_config, f)
121
+
122
+ # Run
123
+ result = fastled_install(dry_run=True, no_interactive=True)
124
+
125
+ # Verify
126
+ self.assertTrue(result)
127
+ with open(".vscode/launch.json") as f:
128
+ data = json.load(f)
129
+ self.assertEqual(len(data["configurations"]), 2)
130
+ self.assertEqual(
131
+ data["configurations"][0]["name"],
132
+ "🎯 Auto Debug (Smart File Detection)",
133
+ )
134
+
135
+ @patch("fastled.install.project_detection.shutil.which")
136
+ @patch("builtins.input")
137
+ def test_parent_directory_detection(self, mock_input, mock_which):
138
+ """Test 5: Find .vscode in parent directories."""
139
+ # Setup
140
+ mock_which.return_value = "/usr/bin/code"
141
+ parent_dir = Path(self.test_dir)
142
+ child_dir = parent_dir / "child" / "grandchild"
143
+ child_dir.mkdir(parents=True)
144
+ (parent_dir / ".vscode").mkdir()
145
+ os.chdir(child_dir)
146
+
147
+ # Test non-interactive mode - should fail
148
+ result = fastled_install(dry_run=True, no_interactive=True)
149
+ self.assertFalse(result) # Should fail in non-interactive
150
+
151
+ # Test interactive mode
152
+ mock_input.side_effect = ["y", "y"] # Yes to parent, yes to extension
153
+ result = fastled_install(dry_run=True, no_interactive=False)
154
+
155
+ # Verify - we should be in parent directory now
156
+ self.assertTrue(result)
157
+ self.assertEqual(Path.cwd(), parent_dir)
158
+
159
+ @patch("fastled.install.project_detection.shutil.which")
160
+ @patch("builtins.input")
161
+ def test_project_generation(self, mock_input, mock_which):
162
+ """Test 6: Generate new VSCode project."""
163
+ # Setup
164
+ mock_which.return_value = "/usr/bin/code"
165
+
166
+ # Test non-interactive mode - should fail
167
+ result = fastled_install(dry_run=True, no_interactive=True)
168
+ self.assertFalse(result) # Should fail without .vscode
169
+
170
+ # Test interactive mode
171
+ mock_input.side_effect = ["y", "y"] # Yes to generate, yes to extension
172
+ result = fastled_install(dry_run=True, no_interactive=False)
173
+
174
+ # Verify
175
+ self.assertTrue(result)
176
+ self.assertTrue(Path(".vscode").exists())
177
+ self.assertTrue(Path(".vscode/launch.json").exists())
178
+ self.assertTrue(Path(".vscode/tasks.json").exists())
179
+
180
+ def test_arduino_content_detection(self):
181
+ """Test 7: Detect existing .ino files."""
182
+ # Create .ino file
183
+ with open("test.ino", "w") as f:
184
+ f.write("void setup() {}")
185
+
186
+ # Test detection
187
+ self.assertTrue(check_existing_arduino_content())
188
+
189
+ # Remove and test examples folder
190
+ os.unlink("test.ino")
191
+ os.makedirs("examples")
192
+ self.assertTrue(check_existing_arduino_content())
193
+
194
+ @patch("fastled.install.project_detection.shutil.which")
195
+ @patch("builtins.input")
196
+ def test_tasks_json_merging(self, mock_input, mock_which):
197
+ """Test 8: Merge FastLED tasks with existing."""
198
+ # Setup
199
+ mock_which.return_value = "/usr/bin/code"
200
+ mock_input.return_value = "y"
201
+ os.makedirs(".vscode")
202
+
203
+ # Create existing tasks.json
204
+ existing_tasks = {
205
+ "version": "2.0.0",
206
+ "tasks": [{"label": "Existing Task", "command": "echo"}],
207
+ }
208
+ with open(".vscode/tasks.json", "w") as f:
209
+ json.dump(existing_tasks, f)
210
+
211
+ # Run
212
+ generate_fastled_tasks()
213
+
214
+ # Verify
215
+ with open(".vscode/tasks.json") as f:
216
+ data = json.load(f)
217
+ self.assertEqual(len(data["tasks"]), 3) # 1 existing + 2 FastLED
218
+ labels = [task["label"] for task in data["tasks"]]
219
+ self.assertIn("Run FastLED (Debug)", labels)
220
+ self.assertIn("Run FastLED (Quick)", labels)
221
+
222
+ def test_launch_json_updates(self):
223
+ """Test 9: Update launch.json configurations."""
224
+ # Setup
225
+ os.makedirs(".vscode")
226
+
227
+ # Run
228
+ update_launch_json_for_arduino()
229
+
230
+ # Verify
231
+ with open(".vscode/launch.json") as f:
232
+ data = json.load(f)
233
+ self.assertEqual(len(data["configurations"]), 1)
234
+ config = data["configurations"][0]
235
+ self.assertEqual(config["name"], "🎯 Auto Debug (Smart File Detection)")
236
+ self.assertEqual(config["type"], "auto-debug")
237
+ self.assertIn("*.ino", config["map"])
238
+
239
+ @patch("fastled.install.project_detection.is_fastled_repository")
240
+ def test_safety_clangd_protection(self, mock_repo):
241
+ """Test 10: 🚨 CRITICAL - clangd safety protection."""
242
+ # Setup
243
+ os.makedirs(".vscode")
244
+
245
+ # Test non-repository
246
+ mock_repo.return_value = False
247
+ update_vscode_settings_for_fastled()
248
+ self.assertFalse(Path(".vscode/settings.json").exists())
249
+
250
+ # Test repository
251
+ mock_repo.return_value = True
252
+ update_vscode_settings_for_fastled()
253
+ self.assertTrue(Path(".vscode/settings.json").exists())
254
+
255
+ with open(".vscode/settings.json") as f:
256
+ data = json.load(f)
257
+ self.assertIn("clangd.arguments", data)
258
+
259
+ @patch("fastled.install.main.check_existing_arduino_content")
260
+ def test_auto_execution_trigger(self, mock_check):
261
+ """Test 11: Post-installation auto-execution."""
262
+ # Setup
263
+ mock_check.return_value = True
264
+ original_argv = sys.argv.copy()
265
+ sys.argv = ["fastled", "--install"]
266
+
267
+ try:
268
+ # We'll test that auto_execute_fastled modifies sys.argv correctly
269
+ # without actually calling main()
270
+ with patch("fastled.app.main") as mock_main:
271
+ mock_main.return_value = 0
272
+ auto_execute_fastled()
273
+
274
+ # Verify
275
+ mock_main.assert_called_once()
276
+ # Check that argv was filtered before calling main
277
+ # The function should have set sys.argv to ['fastled', '.']
278
+ finally:
279
+ sys.argv = original_argv
280
+
281
+ @patch("fastled.install.project_detection.shutil.which")
282
+ def test_no_ide_error_handling(self, mock_which):
283
+ """Test 12: Error when no IDE available."""
284
+ # Setup
285
+ mock_which.return_value = None
286
+
287
+ # Run
288
+ from fastled.install.project_detection import validate_vscode_project
289
+
290
+ result = validate_vscode_project(no_interactive=True)
291
+
292
+ # Verify
293
+ self.assertFalse(result)
294
+
295
+ @patch("subprocess.run")
296
+ @patch("builtins.input")
297
+ def test_examples_installation(self, mock_input, mock_run):
298
+ """Test 13: --project-init examples installation."""
299
+ # Setup
300
+ mock_input.return_value = "y"
301
+ mock_run.return_value = MagicMock(returncode=0)
302
+
303
+ # Run
304
+ from fastled.install.examples_manager import (
305
+ install_fastled_examples_via_project_init,
306
+ )
307
+
308
+ # Test non-interactive mode - should skip
309
+ result = install_fastled_examples_via_project_init(no_interactive=True)
310
+ self.assertFalse(result)
311
+
312
+ # Test interactive mode
313
+ result = install_fastled_examples_via_project_init(no_interactive=False)
314
+ self.assertTrue(result)
315
+ mock_run.assert_called_once()
316
+ call_args = mock_run.call_args[0][0]
317
+ self.assertEqual(call_args, ["fastled", "--project-init"])
318
+
319
+ @patch("fastled.install.extension_manager.download_auto_debug_extension")
320
+ @patch("fastled.install.extension_manager.install_vscode_extensions")
321
+ def test_extension_installation_flow(self, mock_install, mock_download):
322
+ """Test 14: Auto Debug extension prompt/install."""
323
+ # Setup
324
+ mock_download.return_value = Path("test.vsix")
325
+ mock_install.return_value = True
326
+
327
+ # Test dry run
328
+ from fastled.install.extension_manager import install_auto_debug_extension
329
+
330
+ result = install_auto_debug_extension(dry_run=True)
331
+ self.assertTrue(result)
332
+ mock_download.assert_not_called()
333
+
334
+ # Test real install
335
+ result = install_auto_debug_extension(dry_run=False)
336
+ self.assertTrue(result)
337
+ mock_download.assert_called_once()
338
+ mock_install.assert_called_once()
339
+
340
+ @patch("fastled.install.project_detection.is_fastled_repository")
341
+ @patch("fastled.install.project_detection.shutil.which")
342
+ @patch("builtins.input")
343
+ def test_comprehensive_integration(self, mock_input, mock_which, mock_repo):
344
+ """Test 15: End-to-end integration test."""
345
+ # Setup
346
+ mock_which.return_value = "/usr/bin/code"
347
+ mock_input.side_effect = ["y", "y", "y"] # Yes to project, extension, examples
348
+ mock_repo.return_value = False
349
+
350
+ # Create .vscode first for non-interactive test
351
+ os.makedirs(".vscode")
352
+
353
+ # Run full installation in non-interactive mode
354
+ result = fastled_install(dry_run=True, no_interactive=True)
355
+
356
+ # Verify all components
357
+ self.assertTrue(result)
358
+ self.assertTrue(Path(".vscode").exists())
359
+ self.assertTrue(Path(".vscode/launch.json").exists())
360
+ self.assertTrue(Path(".vscode/tasks.json").exists())
361
+
362
+ # Verify tasks have correct content
363
+ with open(".vscode/tasks.json") as f:
364
+ data = json.load(f)
365
+ debug_task = next(
366
+ t for t in data["tasks"] if t["label"] == "Run FastLED (Debug)"
367
+ )
368
+ self.assertIn("--debug", debug_task["args"])
369
+ self.assertIn("--app", debug_task["args"])
370
+
371
+
372
+ if __name__ == "__main__":
373
+ unittest.main()
@@ -0,0 +1,167 @@
1
+ """VSCode configuration generation for FastLED projects."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ def update_launch_json_for_arduino() -> None:
8
+ """Update launch.json with Arduino debugging configuration."""
9
+ launch_json_path = Path.cwd() / ".vscode" / "launch.json"
10
+
11
+ # Default launch configuration
12
+ arduino_config = {
13
+ "name": "🎯 Auto Debug (Smart File Detection)",
14
+ "type": "auto-debug",
15
+ "request": "launch",
16
+ "map": {
17
+ "*.ino": "Arduino: Run .ino with FastLED",
18
+ "*.py": "Python: Current File (UV)",
19
+ },
20
+ }
21
+
22
+ if launch_json_path.exists():
23
+ # Merge with existing
24
+ try:
25
+ with open(launch_json_path, "r") as f:
26
+ data = json.load(f)
27
+ except json.JSONDecodeError:
28
+ data = {"version": "0.2.0", "configurations": []}
29
+ else:
30
+ data = {"version": "0.2.0", "configurations": []}
31
+
32
+ # Check if configuration already exists
33
+ configs = data.get("configurations", [])
34
+ exists = any(cfg.get("name") == arduino_config["name"] for cfg in configs)
35
+
36
+ if not exists:
37
+ configs.insert(0, arduino_config) # Add at the beginning
38
+ data["configurations"] = configs
39
+
40
+ # Write back
41
+ launch_json_path.parent.mkdir(exist_ok=True)
42
+ with open(launch_json_path, "w") as f:
43
+ json.dump(data, f, indent=4)
44
+
45
+ print(f"✅ Updated {launch_json_path}")
46
+
47
+
48
+ def generate_fastled_tasks() -> None:
49
+ """Generate/update tasks.json with FastLED build tasks."""
50
+ tasks_json_path = Path.cwd() / ".vscode" / "tasks.json"
51
+
52
+ # FastLED tasks
53
+ fastled_tasks = [
54
+ {
55
+ "type": "shell",
56
+ "label": "Run FastLED (Debug)",
57
+ "command": "fastled",
58
+ "args": ["${file}", "--debug", "--app"],
59
+ "options": {"cwd": "${workspaceFolder}"},
60
+ "group": {"kind": "build", "isDefault": True},
61
+ "presentation": {
62
+ "echo": True,
63
+ "reveal": "always",
64
+ "focus": True,
65
+ "panel": "new",
66
+ "showReuseMessage": False,
67
+ "clear": True,
68
+ },
69
+ "detail": "Run FastLED with debug mode and app visualization",
70
+ "problemMatcher": [],
71
+ },
72
+ {
73
+ "type": "shell",
74
+ "label": "Run FastLED (Quick)",
75
+ "command": "fastled",
76
+ "args": ["${file}", "--background-update"],
77
+ "options": {"cwd": "${workspaceFolder}"},
78
+ "group": "build",
79
+ "presentation": {
80
+ "echo": True,
81
+ "reveal": "always",
82
+ "focus": True,
83
+ "panel": "new",
84
+ "showReuseMessage": False,
85
+ "clear": True,
86
+ },
87
+ "detail": "Run FastLED with quick background update mode",
88
+ "problemMatcher": [],
89
+ },
90
+ ]
91
+
92
+ if tasks_json_path.exists():
93
+ # Merge with existing
94
+ try:
95
+ with open(tasks_json_path, "r") as f:
96
+ data = json.load(f)
97
+ except json.JSONDecodeError:
98
+ data = {"version": "2.0.0", "tasks": []}
99
+ else:
100
+ data = {"version": "2.0.0", "tasks": []}
101
+
102
+ # Get existing tasks
103
+ existing_tasks = data.get("tasks", [])
104
+ existing_labels = {task.get("label") for task in existing_tasks}
105
+
106
+ # Add new tasks if they don't exist
107
+ for task in fastled_tasks:
108
+ if task["label"] not in existing_labels:
109
+ existing_tasks.append(task)
110
+
111
+ data["tasks"] = existing_tasks
112
+
113
+ # Write back
114
+ tasks_json_path.parent.mkdir(exist_ok=True)
115
+ with open(tasks_json_path, "w") as f:
116
+ json.dump(data, f, indent=4)
117
+
118
+ print(f"✅ Updated {tasks_json_path}")
119
+
120
+
121
+ def update_vscode_settings_for_fastled() -> None:
122
+ """
123
+ 🚨 Repository-only: Apply clangd settings and IntelliSense overrides.
124
+ This should ONLY be called for the actual FastLED repository.
125
+ """
126
+ from .project_detection import is_fastled_repository
127
+
128
+ # Safety check - only apply in actual repository
129
+ if not is_fastled_repository():
130
+ return
131
+ settings_json_path = Path.cwd() / ".vscode" / "settings.json"
132
+
133
+ # FastLED repository-specific settings
134
+ fastled_settings = {
135
+ "clangd.arguments": [
136
+ "--compile-commands-dir=${workspaceFolder}/compile_commands",
137
+ "--header-insertion=never",
138
+ "--clang-tidy",
139
+ "--background-index",
140
+ ],
141
+ "C_Cpp.intelliSenseEngine": "disabled",
142
+ "files.associations": {"*.ino": "cpp", "*.h": "cpp", "*.cpp": "cpp"},
143
+ "editor.formatOnSave": True,
144
+ "editor.formatOnType": True,
145
+ "editor.tabSize": 4,
146
+ "editor.insertSpaces": True,
147
+ }
148
+
149
+ if settings_json_path.exists():
150
+ # Merge with existing
151
+ try:
152
+ with open(settings_json_path, "r") as f:
153
+ data = json.load(f)
154
+ except json.JSONDecodeError:
155
+ data = {}
156
+ else:
157
+ data = {}
158
+
159
+ # Update settings
160
+ data.update(fastled_settings)
161
+
162
+ # Write back
163
+ settings_json_path.parent.mkdir(exist_ok=True)
164
+ with open(settings_json_path, "w") as f:
165
+ json.dump(data, f, indent=4)
166
+
167
+ print(f"✅ Updated {settings_json_path} with FastLED development settings")
fastled/open_browser.py CHANGED
@@ -15,10 +15,13 @@ _playwright_browser_proxy = None
15
15
 
16
16
  def cleanup_playwright_browser() -> None:
17
17
  """Clean up the Playwright browser on exit."""
18
- global _playwright_browser_proxy
19
- if _playwright_browser_proxy:
20
- _playwright_browser_proxy.close()
21
- _playwright_browser_proxy = None
18
+ try:
19
+ global _playwright_browser_proxy
20
+ if _playwright_browser_proxy:
21
+ _playwright_browser_proxy.close()
22
+ _playwright_browser_proxy = None
23
+ except Exception:
24
+ pass
22
25
 
23
26
 
24
27
  # Register cleanup function
fastled/parse_args.py CHANGED
@@ -170,6 +170,24 @@ def parse_args() -> Args:
170
170
  help="Remove all FastLED containers and images",
171
171
  )
172
172
 
173
+ parser.add_argument(
174
+ "--install",
175
+ action="store_true",
176
+ help="Install FastLED development environment with VSCode configuration and Auto Debug extension",
177
+ )
178
+
179
+ parser.add_argument(
180
+ "--dry-run",
181
+ action="store_true",
182
+ help="Run in dry-run mode (simulate actions without making changes)",
183
+ )
184
+
185
+ parser.add_argument(
186
+ "--no-interactive",
187
+ action="store_true",
188
+ help="Run in non-interactive mode (fail instead of prompting for input)",
189
+ )
190
+
173
191
  build_mode = parser.add_mutually_exclusive_group()
174
192
  build_mode.add_argument("--debug", action="store_true", help="Build in debug mode")
175
193
  build_mode.add_argument(
@@ -185,6 +203,28 @@ def parse_args() -> Args:
185
203
 
186
204
  args = parser.parse_args()
187
205
 
206
+ # Auto-enable app mode if debug is used and Playwright cache exists
207
+ if args.debug and not args.app:
208
+ playwright_dir = Path.home() / ".fastled" / "playwright"
209
+ if playwright_dir.exists() and any(playwright_dir.iterdir()):
210
+ print(
211
+ "🎭 Detected Playwright cache - automatically enabling app mode for debug"
212
+ )
213
+ args.app = True
214
+ elif not args.no_interactive:
215
+ # Prompt user to install Playwright only if not in no-interactive mode
216
+ answer = (
217
+ input("Would you like to install the FastLED debugger? [y/n] ")
218
+ .strip()
219
+ .lower()
220
+ )
221
+ if answer in ["y", "yes"]:
222
+ print(
223
+ "📦 To install Playwright, run: pip install playwright && python -m playwright install"
224
+ )
225
+ print("Then run your command again with --app flag")
226
+ sys.exit(0)
227
+
188
228
  # TODO: propagate the library.
189
229
  # from fastled.docker_manager import force_remove_previous
190
230
 
@@ -204,6 +244,11 @@ def parse_args() -> Args:
204
244
  # print(msg)
205
245
  # set_ramdisk_size(args.ram_disk_size)
206
246
 
247
+ # Handle --install early before other processing
248
+ if args.install:
249
+ # Don't process other arguments when --install is used
250
+ return Args.from_namespace(args)
251
+
207
252
  if args.purge:
208
253
  from fastled.docker_manager import DockerManager
209
254