blob: 5102b406fa439b1a32ef9a78e13987ad320bc667 [file]
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import os
import platform
import shlex
import stat
import sys
import pytest
from selenium_manager import _module_name, binary_path, main
def test_linux_dispatches_on_architecture():
assert _module_name("linux", "x86_64") == "selenium_manager_linux_x86_64"
assert _module_name("linux", "aarch64") == "selenium_manager_linux_aarch64"
@pytest.mark.parametrize("machine", ["x86_64", "arm64"])
def test_macos_is_universal(machine):
assert _module_name("darwin", machine) == "selenium_manager_macos"
@pytest.mark.parametrize("machine", ["x86", "AMD64", "ARM64"])
def test_windows_binary_covers_every_architecture(machine):
# The `_x86` suffix names the i686 binary in the package, not the machine it
# is dispatched to; Windows runs it on x64 and arm64 as well.
assert _module_name("win32", machine) == "selenium_manager_windows_x86"
def test_unsupported_linux_architecture_lists_the_supported_ones():
with pytest.raises(RuntimeError) as excinfo:
_module_name("linux", "riscv64")
message = str(excinfo.value)
assert "riscv64" in message
assert "x86_64" in message
assert "aarch64" in message
def test_unsupported_platform():
with pytest.raises(RuntimeError, match="freebsd14"):
_module_name("freebsd14", "x86_64")
def test_module_names_match_their_distribution_names():
# binary_path() turns the module name into the pip install hint by swapping
# underscores for hyphens, so the two have to stay in step.
expected = {
"selenium_manager_linux_x86_64": "selenium-manager-linux-x86-64",
"selenium_manager_linux_aarch64": "selenium-manager-linux-aarch64",
"selenium_manager_macos": "selenium-manager-macos",
"selenium_manager_windows_x86": "selenium-manager-windows-x86",
}
for module_name, distribution in expected.items():
assert module_name.replace("_", "-") == distribution
def test_se_manager_path_overrides_the_platform_package(monkeypatch):
monkeypatch.setenv("SE_MANAGER_PATH", "/opt/sm/selenium-manager")
assert binary_path() == "/opt/sm/selenium-manager"
def test_empty_se_manager_path_is_ignored(monkeypatch):
# An unset variable and one exported as empty should behave the same.
monkeypatch.setenv("SE_MANAGER_PATH", "")
with pytest.raises(RuntimeError):
binary_path()
def test_missing_platform_package_names_the_package_to_install(monkeypatch):
# No platform package is a dependency of this test, so the import failure
# here is the real one a user hits after a partial install.
monkeypatch.delenv("SE_MANAGER_PATH", raising=False)
with pytest.raises(RuntimeError, match="pip install selenium-manager-"):
binary_path()
def test_lookup_failure_is_catchable_by_library_callers(monkeypatch):
# A binding falls back to its vendored manager by catching Exception, so a
# failed lookup must not be a SystemExit that ends the caller's process.
monkeypatch.delenv("SE_MANAGER_PATH", raising=False)
with pytest.raises(Exception) as excinfo:
binary_path()
assert isinstance(excinfo.value, RuntimeError)
def test_launcher_exits_with_the_lookup_error(monkeypatch):
monkeypatch.delenv("SE_MANAGER_PATH", raising=False)
monkeypatch.setattr(sys, "argv", ["selenium-manager"])
with pytest.raises(SystemExit, match="pip install selenium-manager-"):
main()
def test_platform_package_supplies_the_path(tmp_path, monkeypatch):
# Stand up a real platform package rather than patching the import, so what
# is exercised here is the contract an installed wheel actually provides.
module_name = _module_name(sys.platform, platform.machine())
package = tmp_path / module_name
package.mkdir()
(package / "__init__.py").write_text('def binary_path():\n return "/wheels/bin/selenium-manager"\n')
monkeypatch.delenv("SE_MANAGER_PATH", raising=False)
monkeypatch.syspath_prepend(str(tmp_path))
monkeypatch.delitem(sys.modules, module_name, raising=False)
try:
assert binary_path() == "/wheels/bin/selenium-manager"
finally:
sys.modules.pop(module_name, None)
def _install_platform_package(tmp_path, monkeypatch, binary):
module_name = _module_name(sys.platform, platform.machine())
package = tmp_path / module_name
package.mkdir()
(package / "__init__.py").write_text(f"def binary_path():\n return {str(binary)!r}\n")
monkeypatch.delenv("SE_MANAGER_PATH", raising=False)
monkeypatch.syspath_prepend(str(tmp_path))
monkeypatch.delitem(sys.modules, module_name, raising=False)
return module_name
@pytest.mark.skipif(os.name == "nt", reason="Windows has no execute bit")
def test_packaged_binary_is_made_executable_for_path_callers(tmp_path, monkeypatch):
# Callers that run binary_path() themselves never go through main(), so the
# repair for installers that drop the execute bit has to happen here.
binary = tmp_path / "selenium-manager"
binary.write_text("")
binary.chmod(0o644)
module_name = _install_platform_package(tmp_path, monkeypatch, binary)
try:
assert binary_path() == str(binary)
finally:
sys.modules.pop(module_name, None)
assert binary.stat().st_mode & stat.S_IXUSR
@pytest.mark.skipif(os.name == "nt", reason="Windows has no execute bit")
def test_se_manager_path_binary_mode_is_left_alone(tmp_path, monkeypatch):
binary = tmp_path / "selenium-manager"
binary.write_text("")
binary.chmod(0o644)
monkeypatch.setenv("SE_MANAGER_PATH", str(binary))
assert binary_path() == str(binary)
assert stat.S_IMODE(binary.stat().st_mode) == 0o644
def _run_main_recording_avoid_stats(tmp_path, monkeypatch):
record = tmp_path / "avoid-stats.txt"
binary = tmp_path / "selenium-manager"
binary.write_text(f'#!/bin/sh\nprintf "%s" "${{SE_AVOID_STATS-unset}}" > {shlex.quote(str(record))}\n')
binary.chmod(0o755)
monkeypatch.setenv("SE_MANAGER_PATH", str(binary))
monkeypatch.setattr(sys, "argv", ["selenium-manager"])
with pytest.raises(SystemExit) as exit_info:
main()
assert exit_info.value.code == 0
return record.read_text()
@pytest.mark.skipif(os.name == "nt", reason="the stand-in binary is a shell script")
def test_launcher_opts_out_of_usage_stats_by_default(tmp_path, monkeypatch):
# Installing the standalone package is not consent to usage stats, so the
# launcher asks the manager to skip them unless the user says otherwise.
monkeypatch.delenv("SE_AVOID_STATS", raising=False)
assert _run_main_recording_avoid_stats(tmp_path, monkeypatch) == "true"
@pytest.mark.skipif(os.name == "nt", reason="the stand-in binary is a shell script")
def test_launcher_respects_an_explicit_usage_stats_choice(tmp_path, monkeypatch):
monkeypatch.setenv("SE_AVOID_STATS", "false")
assert _run_main_recording_avoid_stats(tmp_path, monkeypatch) == "false"
@pytest.mark.skipif(os.name == "nt", reason="the stand-in binary is a shell script")
def test_empty_usage_stats_choice_is_treated_as_unset(tmp_path, monkeypatch):
monkeypatch.setenv("SE_AVOID_STATS", "")
assert _run_main_recording_avoid_stats(tmp_path, monkeypatch) == "true"