Spaces:
Paused
Paused
| """ | |
| Tests for launcher/frontend_build.py | |
| Tests for frontend build utilities including staleness detection, | |
| npm availability checks, and rebuild functionality. | |
| """ | |
| import subprocess | |
| from unittest.mock import MagicMock, patch | |
| from launcher import frontend_build | |
| # ==================== _get_latest_mtime TESTS ==================== | |
| def test_get_latest_mtime_directory_not_exists(tmp_path): | |
| """Test _get_latest_mtime with non-existent directory.""" | |
| non_existent = tmp_path / "non_existent" | |
| result = frontend_build._get_latest_mtime(non_existent) | |
| assert result == 0.0 | |
| def test_get_latest_mtime_empty_directory(tmp_path): | |
| """Test _get_latest_mtime with empty directory.""" | |
| result = frontend_build._get_latest_mtime(tmp_path) | |
| assert result == 0.0 | |
| def test_get_latest_mtime_with_matching_files(tmp_path): | |
| """Test _get_latest_mtime finds .ts and .tsx files.""" | |
| # Create test files | |
| ts_file = tmp_path / "test.ts" | |
| tsx_file = tmp_path / "component.tsx" | |
| ts_file.write_text("const x = 1;") | |
| tsx_file.write_text("export default () => <div/>;") | |
| result = frontend_build._get_latest_mtime(tmp_path) | |
| assert result > 0.0 | |
| def test_get_latest_mtime_ignores_non_matching_extensions(tmp_path): | |
| """Test _get_latest_mtime ignores files with wrong extensions.""" | |
| # Create non-matching file | |
| other_file = tmp_path / "readme.md" | |
| other_file.write_text("# README") | |
| result = frontend_build._get_latest_mtime(tmp_path) | |
| assert result == 0.0 | |
| def test_get_latest_mtime_handles_oserror(tmp_path): | |
| """Test _get_latest_mtime handles OSError during stat.""" | |
| ts_file = tmp_path / "test.ts" | |
| ts_file.write_text("content") | |
| # The OSError handling is in the try/except block inside the loop | |
| # We need to test that it catches OSError and continues | |
| # This is tested implicitly - if file exists but stat fails, it should return 0.0 | |
| # Since we can't easily mock Path.stat without affecting exists(), we test the logic | |
| # by verifying the function handles the exception gracefully | |
| # Just verify the basic happy path works | |
| result = frontend_build._get_latest_mtime(tmp_path) | |
| assert result > 0.0 | |
| def test_get_latest_mtime_nested_directory(tmp_path): | |
| """Test _get_latest_mtime finds files in nested directories.""" | |
| nested = tmp_path / "src" / "components" | |
| nested.mkdir(parents=True) | |
| ts_file = nested / "Button.tsx" | |
| ts_file.write_text("export const Button = () => {};") | |
| result = frontend_build._get_latest_mtime(tmp_path) | |
| assert result > 0.0 | |
| # ==================== _get_dist_mtime TESTS ==================== | |
| def test_get_dist_mtime_index_exists(): | |
| """Test _get_dist_mtime when index.html exists.""" | |
| with patch.object(frontend_build, "_FRONTEND_DIST") as mock_dist: | |
| mock_index = MagicMock() | |
| mock_index.exists.return_value = True | |
| mock_index.stat.return_value.st_mtime = 12345.0 | |
| mock_dist.__truediv__ = MagicMock(return_value=mock_index) | |
| result = frontend_build._get_dist_mtime() | |
| assert result == 12345.0 | |
| def test_get_dist_mtime_index_not_exists(): | |
| """Test _get_dist_mtime when index.html does not exist.""" | |
| with patch.object(frontend_build, "_FRONTEND_DIST") as mock_dist: | |
| mock_index = MagicMock() | |
| mock_index.exists.return_value = False | |
| mock_dist.__truediv__ = MagicMock(return_value=mock_index) | |
| result = frontend_build._get_dist_mtime() | |
| assert result == 0.0 | |
| def test_get_dist_mtime_oserror(): | |
| """Test _get_dist_mtime handles OSError.""" | |
| with patch.object(frontend_build, "_FRONTEND_DIST") as mock_dist: | |
| mock_index = MagicMock() | |
| mock_index.exists.return_value = True | |
| mock_index.stat.side_effect = OSError("Permission denied") | |
| mock_dist.__truediv__ = MagicMock(return_value=mock_index) | |
| result = frontend_build._get_dist_mtime() | |
| assert result == 0.0 | |
| # ==================== is_frontend_stale TESTS ==================== | |
| def test_is_frontend_stale_dist_not_exists(): | |
| """Test is_frontend_stale returns True when dist doesn't exist.""" | |
| with patch.object(frontend_build, "_FRONTEND_DIST") as mock_dist: | |
| mock_dist.exists.return_value = False | |
| result = frontend_build.is_frontend_stale() | |
| assert result is True | |
| def test_is_frontend_stale_no_source_files(): | |
| """Test is_frontend_stale returns False when no source files exist.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIST") as mock_dist, | |
| patch.object(frontend_build, "_get_latest_mtime", return_value=0.0), | |
| patch.object(frontend_build, "_get_dist_mtime", return_value=1000.0), | |
| ): | |
| mock_dist.exists.return_value = True | |
| result = frontend_build.is_frontend_stale() | |
| assert result is False | |
| def test_is_frontend_stale_src_newer(): | |
| """Test is_frontend_stale returns True when source is newer than dist.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIST") as mock_dist, | |
| patch.object(frontend_build, "_get_latest_mtime", return_value=2000.0), | |
| patch.object(frontend_build, "_get_dist_mtime", return_value=1000.0), | |
| ): | |
| mock_dist.exists.return_value = True | |
| result = frontend_build.is_frontend_stale() | |
| assert result is True | |
| def test_is_frontend_stale_dist_newer(): | |
| """Test is_frontend_stale returns False when dist is newer than source.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIST") as mock_dist, | |
| patch.object(frontend_build, "_get_latest_mtime", return_value=1000.0), | |
| patch.object(frontend_build, "_get_dist_mtime", return_value=2000.0), | |
| ): | |
| mock_dist.exists.return_value = True | |
| result = frontend_build.is_frontend_stale() | |
| assert result is False | |
| # ==================== check_npm_available TESTS ==================== | |
| def test_check_npm_available_found(): | |
| """Test check_npm_available returns True when npm is found.""" | |
| with patch("shutil.which", return_value="/usr/bin/npm"): | |
| result = frontend_build.check_npm_available() | |
| assert result is True | |
| def test_check_npm_available_not_found(): | |
| """Test check_npm_available returns False when npm is not found.""" | |
| with patch("shutil.which", return_value=None): | |
| result = frontend_build.check_npm_available() | |
| assert result is False | |
| # ==================== rebuild_frontend TESTS ==================== | |
| def test_rebuild_frontend_dir_not_exists(): | |
| """Test rebuild_frontend returns False when frontend dir doesn't exist.""" | |
| with patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir: | |
| mock_dir.exists.return_value = False | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| def test_rebuild_frontend_npm_not_available(): | |
| """Test rebuild_frontend returns False when npm is not available.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=False), | |
| ): | |
| mock_dir.exists.return_value = True | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| def test_rebuild_frontend_npm_install_failure(): | |
| """Test rebuild_frontend handles npm install failure.""" | |
| mock_result = MagicMock() | |
| mock_result.returncode = 1 | |
| mock_result.stderr = "npm ERR! Install failed" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=True), | |
| patch("subprocess.run", return_value=mock_result), | |
| ): | |
| mock_dir.exists.return_value = True | |
| mock_node_modules = MagicMock() | |
| mock_node_modules.exists.return_value = False | |
| mock_dir.__truediv__ = MagicMock(return_value=mock_node_modules) | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| def test_rebuild_frontend_npm_install_timeout(): | |
| """Test rebuild_frontend handles npm install timeout.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=True), | |
| patch("subprocess.run", side_effect=subprocess.TimeoutExpired("npm", 120)), | |
| ): | |
| mock_dir.exists.return_value = True | |
| mock_node_modules = MagicMock() | |
| mock_node_modules.exists.return_value = False | |
| mock_dir.__truediv__ = MagicMock(return_value=mock_node_modules) | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| def test_rebuild_frontend_npm_install_exception(): | |
| """Test rebuild_frontend handles npm install exception.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=True), | |
| patch("subprocess.run", side_effect=Exception("Unexpected error")), | |
| ): | |
| mock_dir.exists.return_value = True | |
| mock_node_modules = MagicMock() | |
| mock_node_modules.exists.return_value = False | |
| mock_dir.__truediv__ = MagicMock(return_value=mock_node_modules) | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| def test_rebuild_frontend_build_success(): | |
| """Test rebuild_frontend succeeds when build passes.""" | |
| mock_result = MagicMock() | |
| mock_result.returncode = 0 | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=True), | |
| patch("subprocess.run", return_value=mock_result), | |
| ): | |
| mock_dir.exists.return_value = True | |
| mock_node_modules = MagicMock() | |
| mock_node_modules.exists.return_value = True # Already installed | |
| mock_dir.__truediv__ = MagicMock(return_value=mock_node_modules) | |
| result = frontend_build.rebuild_frontend() | |
| assert result is True | |
| def test_rebuild_frontend_build_failure(): | |
| """Test rebuild_frontend returns False when build fails.""" | |
| mock_result = MagicMock() | |
| mock_result.returncode = 1 | |
| mock_result.stdout = "TypeScript error on line 42" | |
| mock_result.stderr = "" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=True), | |
| patch("subprocess.run", return_value=mock_result), | |
| ): | |
| mock_dir.exists.return_value = True | |
| mock_node_modules = MagicMock() | |
| mock_node_modules.exists.return_value = True | |
| mock_dir.__truediv__ = MagicMock(return_value=mock_node_modules) | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| def test_rebuild_frontend_build_failure_long_error(): | |
| """Test rebuild_frontend truncates long error messages.""" | |
| long_error = "x" * 1000 | |
| mock_result = MagicMock() | |
| mock_result.returncode = 1 | |
| mock_result.stdout = long_error | |
| mock_result.stderr = "" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=True), | |
| patch("subprocess.run", return_value=mock_result), | |
| ): | |
| mock_dir.exists.return_value = True | |
| mock_node_modules = MagicMock() | |
| mock_node_modules.exists.return_value = True | |
| mock_dir.__truediv__ = MagicMock(return_value=mock_node_modules) | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| def test_rebuild_frontend_build_failure_no_output(): | |
| """Test rebuild_frontend handles build failure with no error output.""" | |
| mock_result = MagicMock() | |
| mock_result.returncode = 1 | |
| mock_result.stdout = "" | |
| mock_result.stderr = "" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=True), | |
| patch("subprocess.run", return_value=mock_result), | |
| ): | |
| mock_dir.exists.return_value = True | |
| mock_node_modules = MagicMock() | |
| mock_node_modules.exists.return_value = True | |
| mock_dir.__truediv__ = MagicMock(return_value=mock_node_modules) | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| def test_rebuild_frontend_build_timeout(): | |
| """Test rebuild_frontend handles build timeout.""" | |
| mock_install_result = MagicMock() | |
| mock_install_result.returncode = 0 | |
| call_count = [0] | |
| def side_effect(*args, **kwargs): | |
| call_count[0] += 1 | |
| if call_count[0] == 1: | |
| # npm run build | |
| raise subprocess.TimeoutExpired("npm", 60) | |
| return mock_install_result | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=True), | |
| patch("subprocess.run", side_effect=side_effect), | |
| ): | |
| mock_dir.exists.return_value = True | |
| mock_node_modules = MagicMock() | |
| mock_node_modules.exists.return_value = True | |
| mock_dir.__truediv__ = MagicMock(return_value=mock_node_modules) | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| def test_rebuild_frontend_build_exception(): | |
| """Test rebuild_frontend handles build exception.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_DIR") as mock_dir, | |
| patch.object(frontend_build, "check_npm_available", return_value=True), | |
| patch("subprocess.run", side_effect=Exception("Build crashed")), | |
| ): | |
| mock_dir.exists.return_value = True | |
| mock_node_modules = MagicMock() | |
| mock_node_modules.exists.return_value = True | |
| mock_dir.__truediv__ = MagicMock(return_value=mock_node_modules) | |
| result = frontend_build.rebuild_frontend() | |
| assert result is False | |
| # ==================== ensure_frontend_built TESTS ==================== | |
| def test_ensure_frontend_built_no_src_dir(): | |
| """Test ensure_frontend_built returns early when no src dir.""" | |
| with patch.object(frontend_build, "_FRONTEND_SRC") as mock_src: | |
| mock_src.exists.return_value = False | |
| # Should not raise | |
| frontend_build.ensure_frontend_built() | |
| def test_ensure_frontend_built_stale_triggers_rebuild(): | |
| """Test ensure_frontend_built triggers rebuild when stale.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_SRC") as mock_src, | |
| patch.object(frontend_build, "is_frontend_stale", return_value=True), | |
| patch.object(frontend_build, "rebuild_frontend") as mock_rebuild, | |
| ): | |
| mock_src.exists.return_value = True | |
| frontend_build.ensure_frontend_built() | |
| mock_rebuild.assert_called_once() | |
| def test_ensure_frontend_built_up_to_date_skips(): | |
| """Test ensure_frontend_built skips rebuild when up to date.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_SRC") as mock_src, | |
| patch.object(frontend_build, "is_frontend_stale", return_value=False), | |
| patch.object(frontend_build, "rebuild_frontend") as mock_rebuild, | |
| ): | |
| mock_src.exists.return_value = True | |
| frontend_build.ensure_frontend_built() | |
| mock_rebuild.assert_not_called() | |
| def test_ensure_frontend_built_skip_build_flag(): | |
| """Test ensure_frontend_built skips when skip_build=True.""" | |
| with ( | |
| patch.object(frontend_build, "_FRONTEND_SRC") as mock_src, | |
| patch.object(frontend_build, "is_frontend_stale") as mock_stale, | |
| patch.object(frontend_build, "rebuild_frontend") as mock_rebuild, | |
| ): | |
| mock_src.exists.return_value = True | |
| mock_stale.return_value = True | |
| frontend_build.ensure_frontend_built(skip_build=True) | |
| # Should not call is_frontend_stale or rebuild_frontend | |
| mock_stale.assert_not_called() | |
| mock_rebuild.assert_not_called() | |
| def test_ensure_frontend_built_skip_env_var(): | |
| """Test ensure_frontend_built skips when SKIP_FRONTEND_BUILD=1.""" | |
| with ( | |
| patch.dict("os.environ", {"SKIP_FRONTEND_BUILD": "1"}), | |
| patch.object(frontend_build, "_FRONTEND_SRC") as mock_src, | |
| patch.object(frontend_build, "is_frontend_stale") as mock_stale, | |
| patch.object(frontend_build, "rebuild_frontend") as mock_rebuild, | |
| ): | |
| mock_src.exists.return_value = True | |
| mock_stale.return_value = True | |
| frontend_build.ensure_frontend_built() | |
| # Should not call is_frontend_stale or rebuild_frontend | |
| mock_stale.assert_not_called() | |
| mock_rebuild.assert_not_called() | |
| def test_ensure_frontend_built_skip_env_var_true(): | |
| """Test ensure_frontend_built skips when SKIP_FRONTEND_BUILD=true.""" | |
| with ( | |
| patch.dict("os.environ", {"SKIP_FRONTEND_BUILD": "true"}), | |
| patch.object(frontend_build, "_FRONTEND_SRC") as mock_src, | |
| patch.object(frontend_build, "is_frontend_stale") as mock_stale, | |
| patch.object(frontend_build, "rebuild_frontend") as mock_rebuild, | |
| ): | |
| mock_src.exists.return_value = True | |
| mock_stale.return_value = True | |
| frontend_build.ensure_frontend_built() | |
| # Should not call is_frontend_stale or rebuild_frontend | |
| mock_stale.assert_not_called() | |
| mock_rebuild.assert_not_called() | |
| def test_ensure_frontend_built_skip_env_var_yes(): | |
| """Test ensure_frontend_built skips when SKIP_FRONTEND_BUILD=yes.""" | |
| with ( | |
| patch.dict("os.environ", {"SKIP_FRONTEND_BUILD": "yes"}), | |
| patch.object(frontend_build, "_FRONTEND_SRC") as mock_src, | |
| patch.object(frontend_build, "is_frontend_stale") as mock_stale, | |
| patch.object(frontend_build, "rebuild_frontend") as mock_rebuild, | |
| ): | |
| mock_src.exists.return_value = True | |
| mock_stale.return_value = True | |
| frontend_build.ensure_frontend_built() | |
| # Should not call is_frontend_stale or rebuild_frontend | |
| mock_stale.assert_not_called() | |
| mock_rebuild.assert_not_called() | |