browsergym_form_task / tests /test_browsergym_lifecycle.py
morty649's picture
Deploy BrowserGym form task Space
a484d33
Raw
History Blame Contribute Delete
6.44 kB
import importlib.util
import sys
import threading
import types
from pathlib import Path
import pytest
from server.task_curriculum import TaskCurriculumScheduler
class FakeEnv:
def __init__(self, name, close_error=None):
self.name = name
self.close_error = close_error
self.closed = False
self.reset_count = 0
def close(self):
if self.close_error is not None:
raise self.close_error
self.closed = True
def reset(self, **_kwargs):
self.reset_count += 1
return {"url": f"https://example.test/{self.name}"}, {"goal": self.name}
class FakeState:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
self.goal = kwargs.get("goal", "")
self.current_url = kwargs.get("current_url", "")
self.cum_reward = kwargs.get("cum_reward", 0.0)
def model_copy(self, deep=False):
return FakeState(**self.__dict__)
class FakeObservation:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
class ReverseShuffle:
def shuffle(self, values):
values.reverse()
def load_browsergym_environment_module(monkeypatch):
fake_interfaces = types.ModuleType("openenv.core.env_server.interfaces")
fake_interfaces.Environment = object
fake_models = types.ModuleType("browsergym_env.models")
fake_models.BrowserGymAction = object
fake_models.BrowserGymObservation = FakeObservation
fake_models.BrowserGymState = FakeState
fake_gymnasium = types.ModuleType("gymnasium")
fake_gymnasium.make = lambda *_args, **_kwargs: FakeEnv("initial")
monkeypatch.setitem(sys.modules, "openenv", types.ModuleType("openenv"))
monkeypatch.setitem(sys.modules, "openenv.core", types.ModuleType("openenv.core"))
monkeypatch.setitem(
sys.modules,
"openenv.core.env_server",
types.ModuleType("openenv.core.env_server"),
)
monkeypatch.setitem(
sys.modules, "openenv.core.env_server.interfaces", fake_interfaces
)
monkeypatch.setitem(sys.modules, "browsergym_env", types.ModuleType("browsergym_env"))
monkeypatch.setitem(sys.modules, "browsergym_env.models", fake_models)
monkeypatch.setitem(
sys.modules, "browsergym_env.server", types.ModuleType("browsergym_env.server")
)
monkeypatch.setitem(
sys.modules,
"browsergym_env.server.task_curriculum",
sys.modules["server.task_curriculum"],
)
monkeypatch.setitem(sys.modules, "gymnasium", fake_gymnasium)
module_path = Path(__file__).resolve().parents[1] / "server" / "browsergym_environment.py"
spec = importlib.util.spec_from_file_location(
"browsergym_environment_under_test", module_path
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def make_environment(module, task_name="click-test"):
env = module.BrowserGymEnvironment.__new__(module.BrowserGymEnvironment)
env._lock = threading.RLock()
env.benchmark = "miniwob"
env.task_name = task_name
env.headless = True
env.viewport_width = 1280
env.viewport_height = 720
env.timeout = 10000.0
env.enable_task_curriculum = True
env.task_scheduler = None
env.gym_kwargs = {}
env.env_id = env._build_env_id(task_name)
env.gym_env = FakeEnv(task_name)
env._state = FakeState(episode_id="old", step_count=3, benchmark="miniwob", task_name=task_name)
env._last_obs = None
env._last_info = None
return env
def test_failed_gym_make_during_switch_leaves_old_env_intact(monkeypatch):
module = load_browsergym_environment_module(monkeypatch)
env = make_environment(module)
old_env = env.gym_env
old_state = env._state
def fail_make(_env_id):
raise ValueError("boom")
env._make_gym_env = fail_make
with pytest.raises(ValueError, match="boom"):
env.reset(task_name="enter-text")
assert env.gym_env is old_env
assert old_env.closed is False
assert env.task_name == "click-test"
assert env.env_id == "browsergym/miniwob.click-test"
assert env._state is old_state
def test_reset_switch_commits_after_successful_env_creation(monkeypatch):
module = load_browsergym_environment_module(monkeypatch)
env = make_environment(module)
old_env = env.gym_env
created_env = FakeEnv("enter-text")
env._make_gym_env = lambda _env_id: created_env
observation = env.reset(task_name="enter-text")
assert env.gym_env is created_env
assert old_env.closed is True
assert env.task_name == "enter-text"
assert env.env_id == "browsergym/miniwob.enter-text"
assert env._state.task_name == "enter-text"
assert observation.url == "https://example.test/enter-text"
def test_scheduler_selection_rolls_back_when_switch_creation_fails(monkeypatch):
module = load_browsergym_environment_module(monkeypatch)
env = make_environment(module)
env.task_scheduler = TaskCurriculumScheduler(
tasks=("enter-text",), rng=ReverseShuffle()
)
def fail_make(_env_id):
raise ValueError("boom")
env._make_gym_env = fail_make
with pytest.raises(ValueError, match="boom"):
env.reset()
assert env.task_name == "click-test"
assert env.env_id == "browsergym/miniwob.click-test"
assert env.task_scheduler.next_task() == "enter-text"
def test_close_failure_during_switch_keeps_old_env_referenced(monkeypatch):
module = load_browsergym_environment_module(monkeypatch)
env = make_environment(module)
close_error = RuntimeError("close failed")
old_env = FakeEnv("click-test", close_error=close_error)
new_env = FakeEnv("enter-text")
env.gym_env = old_env
env._make_gym_env = lambda _env_id: new_env
with pytest.raises(RuntimeError, match="close failed"):
env.reset(task_name="enter-text")
assert env.gym_env is old_env
assert new_env.closed is True
assert env.task_name == "click-test"
assert env.env_id == "browsergym/miniwob.click-test"
def test_state_returns_independent_snapshot(monkeypatch):
module = load_browsergym_environment_module(monkeypatch)
env = make_environment(module)
state_snapshot = env.state
state_snapshot.task_name = "mutated"
state_snapshot.step_count = 999
assert state_snapshot is not env._state
assert env._state.task_name == "click-test"
assert env._state.step_count == 3