async def test_service_exists(hass, caplog): """Test importing a pyscript module.""" conf_dir = hass.config.path(FOLDER) file_contents = { f"{conf_dir}/hello.py": """ import xyz2 from xyz2 import f_minus @service def func1(): pyscript.done = [xyz2.f_add(1, 2), xyz2.f_mult(3, 4), xyz2.f_add(10, 20), f_minus(50, 20)] """, # # this will fail to load since import doesn't exist # f"{conf_dir}/bad_import.py": """ import no_such_package @service def func10(): pass """, # # this will fail to load since import has a syntax error # f"{conf_dir}/bad_import2.py": """ import bad_module @service def func11(): pass """, # # This will load, since there is an apps/world config entry # f"{conf_dir}/apps/world.py": """ from xyz2 import * @service def func2(): pyscript.done = [get_x(), get_name(), other_name(), f_add(1, 5), f_mult(3, 6), f_add(10, 30), f_minus(50, 30)] """, # # This will not load, since there is no apps/world2 config entry # f"{conf_dir}/apps/world2.py": """ from xyz2 import * @service def func10(): pass """, f"{conf_dir}/modules/xyz2/__init__.py": """ from .other import f_minus, other_name log.info(f"modules/xyz2 global_ctx={pyscript.get_global_ctx()};") x = 99 def f_add(a, b): return a + b def f_mult(a, b): return a * b def get_x(): return x def get_name(): return __name__ """, f"{conf_dir}/modules/xyz2/other.py": """ def f_minus(a, b): return a - b def other_name(): return __name__ """, # # this module has a syntax error (missing :) # f"{conf_dir}/modules/bad_module.py": """ def func12() pass """, # # this script file should auto-load # f"{conf_dir}/scripts/func13.py": """ @service def func13(): pass """, # # this script file should auto-load # f"{conf_dir}/scripts/a/b/c/d/func14.py": """ @service def func14(): pass log.info(f"func14 global_ctx={pyscript.get_global_ctx()};") """, # # this script file should not auto-load # f"{conf_dir}/scripts/a/b/c/d/#func15.py": """ @service def func15(): pass """, # # this script file should not auto-load # f"{conf_dir}/scripts/#a/b/c/d/func15.py": """ @service def func15(): pass """, } mock_open = MockOpen() for key, value in file_contents.items(): mock_open[key].read_data = value def isfile_side_effect(arg): return arg in file_contents def glob_side_effect(path, recursive=None): result = [] path_re = path.replace("*", "[^/]*").replace(".", "\\.") path_re = path_re.replace("[^/]*[^/]*/", ".*") for this_path in file_contents: if re.match(path_re, this_path): result.append(this_path) return result conf = {"apps": {"world": {}}} with patch( "custom_components.pyscript.os.path.isdir", return_value=True ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch( "custom_components.pyscript.global_ctx.open", mock_open ), patch("custom_components.pyscript.open", mock_open), patch( "homeassistant.config.load_yaml_config_file", return_value={"pyscript": conf} ), patch("custom_components.pyscript.watchdog_start", return_value=None), patch( "custom_components.pyscript.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.global_ctx.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.os.path.isfile" ) as mock_isfile: mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect assert await async_setup_component(hass, "pyscript", {DOMAIN: conf}) notify_q = asyncio.Queue(0) async def state_changed(event): var_name = event.data["entity_id"] if var_name != "pyscript.done": return value = event.data["new_state"].state await notify_q.put(value) hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed) assert not hass.services.has_service("pyscript", "func10") assert not hass.services.has_service("pyscript", "func11") assert hass.services.has_service("pyscript", "func13") assert hass.services.has_service("pyscript", "func14") assert not hass.services.has_service("pyscript", "func15") await hass.services.async_call("pyscript", "func1", {}) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [1 + 2, 3 * 4, 10 + 20, 50 - 20] await hass.services.async_call("pyscript", "func2", {}) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [ 99, "xyz2", "xyz2.other", 1 + 5, 3 * 6, 10 + 30, 50 - 30 ] assert "modules/xyz2 global_ctx=modules.xyz2;" in caplog.text assert "func14 global_ctx=scripts.a.b.c.d.func14;" in caplog.text assert "ModuleNotFoundError: import of no_such_package not allowed" in caplog.text assert "SyntaxError: invalid syntax (bad_module.py, line 2)" in caplog.text
async def test_jupyter_kernel_msgs(hass, caplog): """Test Jupyter kernel messages.""" sock, _ = await setup_script(hass, [dt(2020, 7, 1, 11, 0, 0, 0)], "") hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) # # test the heartbeat loopback with some long and short messages # also send messages to stdin and iopub, which ignore them # for i in range(5): if i & 1: msg = (f"hello {i} " * 40).encode("utf-8") else: msg = f"hello {i}".encode("utf-8") await sock["hb_port"].send(msg) await sock["iopub_port"].send(msg) await sock["stdin_port"].send(msg) assert await sock["hb_port"].recv() == msg # # now send some shell messages and check the responses # reply = await shell_msg(sock, "kernel_info_request", {}) assert reply["header"]["msg_type"] == "kernel_info_reply" reply = await shell_msg(sock, "comm_info_request", {}) assert reply["header"]["msg_type"] == "comm_info_reply" reply = await shell_msg(sock, "history_request", {}) assert reply["header"]["msg_type"] == "history_reply" # # test completions # code = "whi" reply = await shell_msg(sock, "complete_request", { "code": code, "cursor_pos": len(code) }) assert reply["header"]["msg_type"] == "complete_reply" assert reply["content"]["matches"] == ["while"] # # test completions # code = "1+2\n3+4\nwhi" reply = await shell_msg(sock, "complete_request", { "code": code, "cursor_pos": len(code) }) assert reply["header"]["msg_type"] == "complete_reply" assert reply["content"]["matches"] == ["while"] code = "pyscr" reply = await shell_msg(sock, "complete_request", { "code": code, "cursor_pos": len(code) }) assert reply["header"]["msg_type"] == "complete_reply" assert reply["content"]["matches"] == [ "pyscript", "pyscript.config", "pyscript.get_global_ctx", "pyscript.list_global_ctx", "pyscript.set_global_ctx", ] hass.states.async_set("pyscript.f1var1", 0) reply = await shell_msg(sock, "complete_request", { "code": "pyscript.f", "cursor_pos": len("pyscript.f") }) assert reply["header"]["msg_type"] == "complete_reply" assert reply["content"]["matches"] == ["pyscript.f1var1"] hass.states.async_set("pyscript.f1var1", 0, {"attr1": 5, "attr2": 10}) reply = await shell_msg(sock, "complete_request", { "code": "pyscript.f1var1.a", "cursor_pos": len("pyscript.f1var1.a") }) assert reply["header"]["msg_type"] == "complete_reply" assert reply["content"]["matches"] == [ "pyscript.f1var1.attr1", "pyscript.f1var1.attr2" ] # # test is_complete # reply = await shell_msg(sock, "is_complete_request", {"code": "x = 1"}) assert reply["header"]["msg_type"] == "is_complete_reply" assert reply["content"]["status"] == "complete" reply = await shell_msg(sock, "is_complete_request", {"code": "def func():\n pass"}) assert reply["header"]["msg_type"] == "is_complete_reply" assert reply["content"]["status"] == "incomplete" reply = await shell_msg(sock, "is_complete_request", {"code": "def func():\n pass\n"}) assert reply["header"]["msg_type"] == "is_complete_reply" assert reply["content"]["status"] == "complete" reply = await shell_msg(sock, "is_complete_request", {"code": "x = "}) assert reply["header"]["msg_type"] == "is_complete_reply" assert reply["content"]["status"] == "invalid" reply = await shell_msg(sock, "is_complete_request", {"code": "if 1:\n"}) assert reply["header"]["msg_type"] == "is_complete_reply" assert reply["content"]["status"] == "incomplete" # # test code execution # reply = await shell_msg(sock, "execute_request", {"code": "x = 123; x + 1 + 2"}, execute=True) assert reply["content"]["data"]["text/plain"] == "126" reply = await shell_msg(sock, "execute_request", {"code": "import math; x + 5"}, execute=True) assert reply["content"]["data"]["text/plain"] == "128" # # do a reload to make sure our global context is preserved # with patch("homeassistant.config.load_yaml_config_file", return_value={}): await hass.services.async_call("pyscript", "reload", {}, blocking=True) reply = await shell_msg(sock, "execute_request", {"code": "x + 10"}, execute=True) assert reply["content"]["data"]["text/plain"] == "133" # # test completion of object attribute now that we've loaded math above # code = "import math; math.sq" reply = await shell_msg(sock, "complete_request", { "code": code, "cursor_pos": len(code) }) assert reply["header"]["msg_type"] == "complete_reply" assert reply["content"]["matches"] == ["math.sqrt"] # # run-time error # reply = await shell_msg(sock, "execute_request", {"code": "xyz"}, execute=True) assert reply["content"]["evalue"] == "name 'xyz' is not defined" # # syntax error # reply = await shell_msg(sock, "execute_request", {"code": "1 + "}, execute=True) assert reply["content"]["evalue"] == "invalid syntax (jupyter_0, line 1)" hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await shutdown(sock)
async def setup_subaru_integration( hass, vehicle_list=None, vehicle_data=None, vehicle_status=None, connect_success=True, ): """Create Subaru entry.""" assert await async_setup_component(hass, DOMAIN, {}) config_entry = MockConfigEntry( domain=DOMAIN, data=TEST_CONFIG, options=TEST_OPTIONS, entry_id=1, ) config_entry.add_to_hass(hass) with patch( "custom_components.subaru.SubaruAPI.connect", return_value=connect_success, side_effect=None if connect_success else InvalidCredentials("Invalid Credentials"), ), patch( "custom_components.subaru.SubaruAPI.get_vehicles", return_value=vehicle_list, ), patch( "custom_components.subaru.SubaruAPI.vin_to_name", return_value=vehicle_data[VEHICLE_NAME], ), patch( "custom_components.subaru.SubaruAPI.get_api_gen", return_value=vehicle_data[VEHICLE_API_GEN], ), patch( "custom_components.subaru.SubaruAPI.get_ev_status", return_value=vehicle_data[VEHICLE_HAS_EV], ), patch( "custom_components.subaru.SubaruAPI.get_res_status", return_value=vehicle_data[VEHICLE_HAS_REMOTE_START], ), patch( "custom_components.subaru.SubaruAPI.get_remote_status", return_value=vehicle_data[VEHICLE_HAS_REMOTE_SERVICE], ), patch( "custom_components.subaru.SubaruAPI.get_safety_status", return_value=vehicle_data[VEHICLE_HAS_SAFETY_SERVICE], ), patch( "custom_components.subaru.SubaruAPI.get_data", return_value=vehicle_status, ), patch( "custom_components.subaru.SubaruAPI.update", ), patch( "custom_components.subaru.SubaruAPI.fetch", ): success = await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() if success: return config_entry return None
async def test_api(hass): """Test API.""" MOCK_ENTRY.add_to_hass(hass) implementation = ZoomOAuth2Implementation( hass, DOMAIN, MOCK_ENTRY.data[CONF_CLIENT_ID], MOCK_ENTRY.data[CONF_CLIENT_SECRET], OAUTH2_AUTHORIZE, OAUTH2_TOKEN, MOCK_ENTRY.data[CONF_VERIFICATION_TOKEN], ) api = ZoomAPI( config_entry_oauth2_flow.OAuth2Session(hass, MOCK_ENTRY, implementation)) assert await api.async_get_access_token() == MOCK_TOKEN with patch( "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_request", return_value=AiohttpClientMockResponse( "get", "zoom_url", status=HTTPStatus.OK, json={ "id": "test", "first_name": "test" }, ), ): await api.async_get_contact_user_profile("test") with patch( "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_request", return_value=AiohttpClientMockResponse( "get", "zoom_url", status=HTTPStatus.OK, json={ "next_page_token": "", "contacts": [{ "id": "test", "first_name": "test" }], }, ), ): await api.async_get_contacts() with patch( "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_request", return_value=AiohttpClientMockResponse( "get", "zoom_url", status=HTTPStatus.OK, json={ "id": "test", "first_name": "test" }, ), ): await api.async_get_my_user_profile()
async def setup_script(hass, now, source, no_connect=False): """Initialize and load the given pyscript.""" scripts = [ "/some/config/dir/pyscripts/hello.py", ] with patch( "custom_components.pyscript.os.path.isdir", return_value=True), patch( "custom_components.pyscript.glob.iglob", return_value=scripts), patch( "custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True, ), patch( "custom_components.pyscript.trigger.dt_now", return_value=now), patch( "homeassistant.config.load_yaml_config_file", return_value={}), patch( "custom_components.pyscript.install_requirements", return_value=None, ): assert await async_setup_component(hass, "pyscript", {DOMAIN: {}}) # # I'm not sure how to run the mock all the time, so just force the dt_now() # trigger function to return the given list of times in now. # def return_next_time(): nonlocal now if isinstance(now, list): if len(now) > 1: return now.pop(0) return now[0] return now trigger.__dict__["dt_now"] = return_next_time kernel_state_var = "pyscript.jupyter_ports_1234" kernel_cfg = { "ip": "127.0.0.1", "key": SECRET_KEY.decode("utf-8"), "signature_scheme": "hmac-sha256", "state_var": kernel_state_var, } if no_connect: kernel_cfg["no_connect_timeout"] = 0.0 await hass.services.async_call("pyscript", "jupyter_kernel_start", kernel_cfg) while True: ports_state = hass.states.get(kernel_state_var) if ports_state is not None: break await asyncio.sleep(2e-3) port_nums = json.loads(ports_state.state) sock = {} if no_connect: return sock, port_nums for name in PORT_NAMES: kernel_reader, kernel_writer = await asyncio.open_connection( "127.0.0.1", port_nums[name]) sock[name] = ZmqSocket(kernel_reader, kernel_writer, "ROUTER") await sock[name].handshake() return sock, port_nums
def bypass_package_install_fixture(): """Bypass package installation.""" with patch( "custom_components.pyscript.requirements.async_process_requirements" ): yield
async def test_install_requirements(hass, caplog): """Test install_requirements function.""" with patch( "custom_components.pyscript.requirements.process_all_requirements" ) as process_requirements, patch( "custom_components.pyscript.requirements.async_process_requirements" ) as ha_install_requirements: entry = MockConfigEntry(domain=DOMAIN, data={CONF_ALLOW_ALL_IMPORTS: True}) entry.add_to_hass(hass) # Check that packages get installed correctly process_requirements.return_value = { "my-package-name": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: "2.0.1", ATTR_INSTALLED_VERSION: None, }, "my-package-name-alternate": { ATTR_SOURCES: [f"{PYSCRIPT_FOLDER}/requirements.txt"], ATTR_VERSION: "2.0.1", ATTR_INSTALLED_VERSION: None, }, } await install_requirements(hass, entry, PYSCRIPT_FOLDER) await hass.async_block_till_done() assert ha_install_requirements.called assert ha_install_requirements.call_args[0][2] == [ "my-package-name==2.0.1", "my-package-name-alternate==2.0.1", ] assert CONF_INSTALLED_PACKAGES in entry.data assert entry.data[CONF_INSTALLED_PACKAGES] == { "my-package-name": "2.0.1", "my-package-name-alternate": "2.0.1", } # Check that we stop tracking packages whose version no longer matches what # we have stored (see previous line for what we currently have stored) ha_install_requirements.reset_mock() caplog.clear() process_requirements.return_value = { "my-package-name": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: "2.0.1", ATTR_INSTALLED_VERSION: "2.0.1", }, "my-package-name-alternate": { ATTR_SOURCES: [f"{PYSCRIPT_FOLDER}/requirements.txt"], ATTR_VERSION: "2.0.1", ATTR_INSTALLED_VERSION: "1.2.1", }, } await install_requirements(hass, entry, PYSCRIPT_FOLDER) await hass.async_block_till_done() assert not ha_install_requirements.called assert caplog.record_tuples == [( "custom_components.pyscript", logging.WARNING, ("Version '2.0.1' for package 'my-package-name-alternate' detected " "in '['tests/test_data/test_requirements/requirements.txt']' will " "be ignored in favor of the version '1.2.1' which was installed " "outside of pyscript"), )] assert entry.data[CONF_INSTALLED_PACKAGES] == { "my-package-name": "2.0.1" } # Check that version upgrades are handled if the version was installed # by us before ha_install_requirements.reset_mock() caplog.clear() process_requirements.return_value = { "my-package-name": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: "2.2.0", ATTR_INSTALLED_VERSION: "2.0.1", }, } await install_requirements(hass, entry, PYSCRIPT_FOLDER) await hass.async_block_till_done() assert ha_install_requirements.called assert ha_install_requirements.call_args[0][2] == [ "my-package-name==2.2.0" ] assert entry.data[CONF_INSTALLED_PACKAGES] == { "my-package-name": "2.2.0" } # Check that we don't install untracked but existing packages ha_install_requirements.reset_mock() caplog.clear() process_requirements.return_value = { "my-package-name-alternate": { ATTR_SOURCES: [f"{PYSCRIPT_FOLDER}/requirements.txt"], ATTR_VERSION: "2.0.1", ATTR_INSTALLED_VERSION: "1.2.1", }, } await install_requirements(hass, entry, PYSCRIPT_FOLDER) await hass.async_block_till_done() assert not ha_install_requirements.called assert entry.data[CONF_INSTALLED_PACKAGES] == { "my-package-name": "2.2.0" } # Check that we can downgrade as long as we installed the package ha_install_requirements.reset_mock() caplog.clear() process_requirements.return_value = { "my-package-name": { ATTR_SOURCES: [f"{PYSCRIPT_FOLDER}/requirements.txt"], ATTR_VERSION: "2.0.1", ATTR_INSTALLED_VERSION: "2.2.0", }, } await install_requirements(hass, entry, PYSCRIPT_FOLDER) await hass.async_block_till_done() assert ha_install_requirements.called assert ha_install_requirements.call_args[0][2] == [ "my-package-name==2.0.1" ] assert entry.data[CONF_INSTALLED_PACKAGES] == { "my-package-name": "2.0.1" }
async def test_reload(hass, caplog): """Test reload a pyscript module.""" conf_dir = hass.config.path(FOLDER) file_contents = { f"{conf_dir}/hello.py": """ import xyz2 from xyz2 import xyz # # ensure a regular script doesn't have pyscript.app_config set # try: x = pyscript.app_config assert False except NameError: pass log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} xyz={xyz} xyz2.xyz={xyz2.xyz}") @service def func1(): pass """, # # This will load, since there is an apps/world config entry # f"{conf_dir}/apps/world.py": """ from xyz2 import * log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} xyz={xyz}") @service def func2(): pass """, # # This will load, since there is an apps/world2 config entry # f"{conf_dir}/apps/world2/__init__.py": """ from .other import * assert pyscript.config['apps']['world2'] == pyscript.app_config log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} var1={pyscript.config['apps']['world2']['var1']}, other_abc={other_abc}") @service def func3(): pass """, f"{conf_dir}/apps/world2/other.py": """ other_abc = 987 log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()}") # # ensure a sub file in the app doesn't have pyscript.app_config set # try: x = pyscript.app_config assert False except NameError: pass @time_trigger("shutdown") def shutdown(trigger_time=None): log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} shutdown trigger_time={trigger_time}") """, f"{conf_dir}/modules/xyz2/__init__.py": """ from .other import xyz # # ensure a module doesn't have pyscript.app_config set # try: x = pyscript.app_config assert False except NameError: pass log.info(f"modules/xyz2 global_ctx={pyscript.get_global_ctx()};") """, f"{conf_dir}/modules/xyz2/other.py": """ log.info(f"modules/xyz2/other global_ctx={pyscript.get_global_ctx()};") xyz = 123 """, # # these shouldn't load since the package takes precedence # f"{conf_dir}/modules/xyz2.py": """ log.info(f"BOTCH shouldn't load {__name__}") """, f"{conf_dir}/apps/world2.py": """ log.info(f"BOTCH shouldn't load {__name__}") """, } mock_open = MockOpen() for key, value in file_contents.items(): mock_open[key].read_data = value def isfile_side_effect(arg): return arg in file_contents def glob_side_effect(path, recursive=None): result = [] path_re = path.replace("*", "[^/]*").replace(".", "\\.") path_re = path_re.replace("[^/]*[^/]*/", ".*") for this_path in file_contents: if re.match(path_re, this_path): result.append(this_path) return result conf = {"apps": {"world": {}, "world2": {"var1": 100}}} with patch( "custom_components.pyscript.os.path.isdir", return_value=True ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch( "custom_components.pyscript.global_ctx.open", mock_open ), patch("custom_components.pyscript.open", mock_open), patch( "homeassistant.util.yaml.loader.open", mock_open ), patch("homeassistant.config.load_yaml_config_file", return_value={"pyscript": conf}), patch( "custom_components.pyscript.watchdog_start", return_value=None), patch( "custom_components.pyscript.os.path.getmtime", return_value=1000 ), patch( "custom_components.pyscript.global_ctx.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.os.path.isfile" ) as mock_isfile: mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect assert await async_setup_component(hass, "pyscript", {DOMAIN: conf}) notify_q = asyncio.Queue(0) async def state_changed(event): var_name = event.data["entity_id"] if var_name != "pyscript.done": return value = event.data["new_state"].state await notify_q.put(value) hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed) assert hass.services.has_service("pyscript", "func1") assert hass.services.has_service("pyscript", "func2") assert hass.services.has_service("pyscript", "func3") assert "modules/xyz2 global_ctx=modules.xyz2;" in caplog.text assert "modules/xyz2/other global_ctx=modules.xyz2.other;" in caplog.text assert "hello global_ctx=file.hello xyz=123 xyz2.xyz=123" in caplog.text assert "world2.other global_ctx=apps.world2.other" in caplog.text assert "world2 global_ctx=apps.world2 var1=100, other_abc=987" in caplog.text # # add a new script file # file_contents[f"{conf_dir}/hello2.py"] = """ log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()};") @service def func20(): pass """ mock_open[f"{conf_dir}/hello2.py"].read_data = file_contents[ f"{conf_dir}/hello2.py"] # # should not load the new script if we reload something else # await hass.services.async_call("pyscript", "reload", {"global_ctx": "file.hello"}, blocking=True) assert not hass.services.has_service("pyscript", "func20") assert "hello2 global_ctx=file.hello2;" not in caplog.text # # should load new file # await hass.services.async_call("pyscript", "reload", {}, blocking=True) assert hass.services.has_service("pyscript", "func20") assert "hello2 global_ctx=file.hello2;" in caplog.text # # delete the script file # del file_contents[f"{conf_dir}/hello2.py"] # # should not delete the script file if we reload something else # await hass.services.async_call("pyscript", "reload", {"global_ctx": "file.hello"}, blocking=True) assert hass.services.has_service("pyscript", "func20") # # should delete the script file # await hass.services.async_call("pyscript", "reload", {}, blocking=True) assert not hass.services.has_service("pyscript", "func20") # # change a module file and confirm the parent script is reloaded too # file_contents[f"{conf_dir}/modules/xyz2/other.py"] = """ log.info(f"modules/xyz2/other global_ctx={pyscript.get_global_ctx()};") xyz = 456 """ mock_open[ f"{conf_dir}/modules/xyz2/other.py"].read_data = file_contents[ f"{conf_dir}/modules/xyz2/other.py"] await hass.services.async_call("pyscript", "reload", {}, blocking=True) assert "hello global_ctx=file.hello xyz=456 xyz2.xyz=456" in caplog.text # # change the app config # conf["apps"]["world2"]["var1"] = 200 await hass.services.async_call("pyscript", "reload", {}, blocking=True) assert "world2 global_ctx=apps.world2 var1=200, other_abc=987" in caplog.text assert "world2.other global_ctx=apps.world2.other shutdown trigger_time=shutdown" in caplog.text # # change a module inside an app # file_contents[f"{conf_dir}/apps/world2/other.py"] = """ other_abc = 654 log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()}") @time_trigger("shutdown") def shutdown(trigger_time=None): log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} shutdown_new trigger_time={trigger_time}") """ mock_open[ f"{conf_dir}/apps/world2/other.py"].read_data = file_contents[ f"{conf_dir}/apps/world2/other.py"] await hass.services.async_call("pyscript", "reload", {}, blocking=True) assert "world2 global_ctx=apps.world2 var1=200, other_abc=654" in caplog.text assert "world2.other global_ctx=apps.world2.other shutdown trigger_time=shutdown" in caplog.text # # now confirm certain files reloaded the correct number of times, # and reload everything a few times # for i in range(3): assert caplog.text.count( "world global_ctx=apps.world xyz=") == 2 + i assert caplog.text.count( "world2 global_ctx=apps.world2 var1=") == 3 + i assert caplog.text.count( "hello global_ctx=file.hello xyz=") == 4 + i assert caplog.text.count( "modules/xyz2/other global_ctx=modules.xyz2.other") == 2 + i assert caplog.text.count( "modules/xyz2 global_ctx=modules.xyz2") == 2 + i assert (caplog.text.count( "world2.other global_ctx=apps.world2.other shutdown trigger_time=shutdown" ) == 2) assert (caplog.text.count( "world2.other global_ctx=apps.world2.other shutdown_new trigger_time=shutdown" ) == i) if i < 2: await hass.services.async_call("pyscript", "reload", {"global_ctx": "*"}, blocking=True) # # make sure files that shouldn't load were not loaded # assert "BOTCH shouldn't load" not in caplog.text
async def test_install_unpinned_requirements(hass, caplog): """Test install_requirements function with unpinned versions.""" with patch( "custom_components.pyscript.requirements.process_all_requirements" ) as process_requirements, patch( "custom_components.pyscript.requirements.async_process_requirements" ) as ha_install_requirements: entry = MockConfigEntry(domain=DOMAIN, data={CONF_ALLOW_ALL_IMPORTS: True}) entry.add_to_hass(hass) # Check that unpinned version gets skipped because a version is already # installed process_requirements.return_value = { "my-package-name": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: UNPINNED_VERSION, ATTR_INSTALLED_VERSION: "2.0.1", }, } await install_requirements(hass, entry, PYSCRIPT_FOLDER) await hass.async_block_till_done() assert not ha_install_requirements.called # Check that unpinned version gets installed because it isn't already # installed process_requirements.return_value = { "my-package-name": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: UNPINNED_VERSION, ATTR_INSTALLED_VERSION: None, }, "my-package-name-1": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: "2.0.1", ATTR_INSTALLED_VERSION: None, }, } await install_requirements(hass, entry, PYSCRIPT_FOLDER) await hass.async_block_till_done() assert ha_install_requirements.called assert ha_install_requirements.call_args[0][2] == [ "my-package-name", "my-package-name-1==2.0.1" ] # my-package-name will show as not installed and therefore won't be included assert entry.data[CONF_INSTALLED_PACKAGES] == { "my-package-name-1": "2.0.1" } # Check that entry.data[CONF_INSTALLED_PACKAGES] gets updated with a version number # when unpinned version was requested with patch("custom_components.pyscript.requirements.installed_version", return_value="1.1.1"): process_requirements.return_value = { "my-package-name": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: UNPINNED_VERSION, ATTR_INSTALLED_VERSION: None, }, "my-package-name-1": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: "2.0.1", ATTR_INSTALLED_VERSION: None, }, } await install_requirements(hass, entry, PYSCRIPT_FOLDER) await hass.async_block_till_done() assert ha_install_requirements.called assert ha_install_requirements.call_args[0][2] == [ "my-package-name", "my-package-name-1==2.0.1" ] assert entry.data[CONF_INSTALLED_PACKAGES] == { "my-package-name": "1.1.1", "my-package-name-1": "2.0.1", } # Check that package gets removed from entry.data[CONF_INSTALLED_PACKAGES] when it was # previously installed by pyscript but version was changed presumably by another system process_requirements.return_value = { "my-package-name": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: UNPINNED_VERSION, ATTR_INSTALLED_VERSION: "2.0.0", }, "my-package-name-1": { ATTR_SOURCES: [ f"{PYSCRIPT_FOLDER}/requirements.txt", f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt", ], ATTR_VERSION: "2.0.1", ATTR_INSTALLED_VERSION: None, }, } await install_requirements(hass, entry, PYSCRIPT_FOLDER) await hass.async_block_till_done() assert ha_install_requirements.called assert ha_install_requirements.call_args[0][2] == [ "my-package-name-1==2.0.1" ] assert entry.data[CONF_INSTALLED_PACKAGES] == { "my-package-name-1": "2.0.1" }
async def test_tasks(hass, caplog): """Test starting tasks.""" conf_dir = hass.config.path(FOLDER) file_contents = { f"{conf_dir}/hello.py": """ # # check starting multiple tasks, each stopping the prior one # def task1(cnt, last): task.unique('task1') if not last: task.sleep(10) log.info(f"finished task1, cnt={cnt}") for cnt in range(10): task.create(task1, cnt, cnt == 9) # # check the return value after wait # def task2(arg): return 2 * arg t2a = task.create(task2, 21) t2b = task.create(task2, 51) done, pending = task.wait({t2a, t2b}) log.info(f"task2() results = {[t2a.result(), t2b.result()]}, len(done) = {len(done)};") # # check the return value with a regular function # @pyscript_compile def task3(arg): return 2 * arg t3a = task.create(task3, 22) t3b = task.create(task3, 52) done, pending = task.wait({t3a, t3b}) log.info(f"task3() results = {[t3a.result(), t3b.result()]}, len(done) = {len(done)};") # # check that we can do a done callback # def task4(arg): task.wait_until(state_trigger="pyscript.var4 == '1'") return 2 * arg def callback4a(arg): log.info(f"callback4a arg = {arg}") def callback4b(arg): log.info(f"callback4b arg = {arg}") def callback4c(arg): log.info(f"callback4c arg = {arg}") t4 = task.create(task4, 23) task.add_done_callback(t4, callback4a, 26) task.add_done_callback(t4, callback4b, 101) task.add_done_callback(t4, callback4c, 200) task.add_done_callback(t4, callback4a, 25) task.add_done_callback(t4, callback4c, 201) task.add_done_callback(t4, callback4b, 100) task.add_done_callback(t4, callback4a, 24) task.remove_done_callback(t4, callback4c) task.remove_done_callback(t4, task4) pyscript.var4 = 1 done, pending = task.wait({t4}) log.info(f"task4() result = {t4.result()}, len(done) = {len(done)};") # # make sure we can't cancel a non-user task # try: task.cancel() except TypeError as exc: log.info(f"task.cancel: {exc}") # # check that we can cancel ourselves # def task5(arg=None): log.info(f"task5 arg = {arg}") task.cancel() log.info(f"task5 BOTCH") t5 = task.create(task5, 83) done, pending = task.wait({t5}) """, } mock_open = MockOpen() for key, value in file_contents.items(): mock_open[key].read_data = value def isfile_side_effect(arg): return arg in file_contents def glob_side_effect(path, recursive=None): result = [] path_re = path.replace("*", "[^/]*").replace(".", "\\.") path_re = path_re.replace("[^/]*[^/]*/", ".*") for this_path in file_contents: if re.match(path_re, this_path): result.append(this_path) return result conf = {"apps": {"world": {}}} with patch( "custom_components.pyscript.os.path.isdir", return_value=True ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch( "custom_components.pyscript.global_ctx.open", mock_open ), patch("custom_components.pyscript.open", mock_open), patch( "homeassistant.config.load_yaml_config_file", return_value={"pyscript": conf} ), patch("custom_components.pyscript.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.watchdog_start", return_value=None), patch( "custom_components.pyscript.global_ctx.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.os.path.isfile" ) as mock_isfile: mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect assert await async_setup_component(hass, "pyscript", {DOMAIN: conf}) notify_q = asyncio.Queue(0) async def state_changed(event): var_name = event.data["entity_id"] if var_name != "pyscript.done": return value = event.data["new_state"].state await notify_q.put(value) hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed) assert caplog.text.count("finished task1, cnt=9") == 1 assert "task2() results = [42, 102], len(done) = 2;" in caplog.text assert "task3() results = [44, 104], len(done) = 2;" in caplog.text assert "task4() result = 46, len(done) = 1;" in caplog.text assert caplog.text.count("callback4a arg =") == 1 assert "callback4a arg = 24" in caplog.text assert caplog.text.count("callback4b arg =") == 1 assert "callback4b arg = 100" in caplog.text assert "callback4c arg =" not in caplog.text assert caplog.text.count("is not a user-started task") == 1 assert "task5 arg = 83" in caplog.text assert "task5 BOTCH" not in caplog.text
async def setup_script(hass, notify_q, notify_q2, now, source, config=None): """Initialize and load the given pyscript.""" conf_dir = hass.config.path(FOLDER) file_contents = {f"{conf_dir}/hello.py": source} mock_open = MockOpen() for key, value in file_contents.items(): mock_open[key].read_data = value def isfile_side_effect(arg): return arg in file_contents def glob_side_effect(path, recursive=None): result = [] path_re = path.replace("*", "[^/]*").replace(".", "\\.") path_re = path_re.replace("[^/]*[^/]*/", ".*") for this_path in file_contents: if re.match(path_re, this_path): result.append(this_path) return result if not config: config = {DOMAIN: {CONF_ALLOW_ALL_IMPORTS: True}} with patch( "custom_components.pyscript.os.path.isdir", return_value=True ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch( "custom_components.pyscript.global_ctx.open", mock_open ), patch( "custom_components.pyscript.trigger.dt_now", return_value=now ), patch("custom_components.pyscript.open", mock_open), patch( "homeassistant.config.load_yaml_config_file", return_value=config ), patch( "custom_components.pyscript.install_requirements", return_value=None, ), patch("custom_components.pyscript.watchdog_start", return_value=None), patch( "custom_components.pyscript.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.global_ctx.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.os.path.isfile" ) as mock_isfile: mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect assert await async_setup_component(hass, "pyscript", config) # # I'm not sure how to run the mock all the time, so just force the dt_now() # trigger function to return the given list of times in now. # def return_next_time(): nonlocal now if isinstance(now, list): if len(now) > 1: return now.pop(0) return now[0] return now trigger.__dict__["dt_now"] = return_next_time if notify_q or notify_q2: async def state_changed(event): var_name = event.data["entity_id"] if var_name == "pyscript.done": value = event.data["new_state"].state if notify_q: await notify_q.put(value) if var_name == "pyscript.done2": value = event.data["new_state"].state if notify_q2: await notify_q2.put(value) hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed)
async def test_reload(hass, caplog): """Test reload.""" notify_q = asyncio.Queue(0) now = dt(2020, 7, 1, 11, 59, 59, 999999) source0 = """ seq_num = 0 @time_trigger def func_startup_sync(): global seq_num seq_num += 1 log.info(f"func_startup_sync setting pyscript.done = {seq_num}") pyscript.done = seq_num @service @state_trigger("pyscript.f1var1 == '1'") def func9(var_name=None, value=None): global seq_num seq_num += 1 log.info(f"func9 var = {var_name}, value = {value}") pyscript.done = [seq_num, var_name, int(value)] """ source1 = """ seq_num = 10 @time_trigger("startup") def func_startup_sync(): global seq_num seq_num += 1 log.info(f"func_startup_sync setting pyscript.done = {seq_num}") pyscript.done = seq_num @service @state_trigger("pyscript.f5var1 == '1'") def func5(var_name=None, value=None): global seq_num seq_num += 1 log.info(f"func5 var = {var_name}, value = {value}") pyscript.done = [seq_num, var_name, int(value)] """ await setup_script(hass, notify_q, now, source0) # # run and reload 6 times with different source files to make sure seqNum # gets reset, autostart of func_startup_sync happens and triggers work each time # # first time: fire event to startup triggers and run func_startup_sync # hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) for i in range(6): if i & 1: seq_num = 10 assert not hass.services.has_service("pyscript", "func9") assert hass.services.has_service("pyscript", "reload") assert hass.services.has_service("pyscript", "func5") seq_num += 1 assert literal_eval(await wait_until_done(notify_q)) == seq_num seq_num += 1 # initialize the trigger and active variables hass.states.async_set("pyscript.f5var1", 0) # try some values that shouldn't work, then one that does hass.states.async_set("pyscript.f5var1", "string") hass.states.async_set("pyscript.f5var1", 1) assert literal_eval(await wait_until_done(notify_q)) == [ seq_num, "pyscript.f5var1", 1, ] assert "func5 var = pyscript.f5var1, value = 1" in caplog.text next_source = source0 else: seq_num = 0 assert hass.services.has_service("pyscript", "func9") assert hass.services.has_service("pyscript", "reload") assert not hass.services.has_service("pyscript", "func5") seq_num += 1 assert literal_eval(await wait_until_done(notify_q)) == seq_num seq_num += 1 # initialize the trigger and active variables hass.states.async_set("pyscript.f1var1", 0) # try some values that shouldn't work, then one that does hass.states.async_set("pyscript.f1var1", "string") hass.states.async_set("pyscript.f1var1", 1) assert literal_eval(await wait_until_done(notify_q)) == [ seq_num, "pyscript.f1var1", 1, ] assert "func9 var = pyscript.f1var1, value = 1" in caplog.text next_source = source1 # # now reload the other source file # scripts = [ "/hello.py", ] with patch( "custom_components.pyscript.os.path.isdir", return_value=True ), patch( "custom_components.pyscript.glob.iglob", return_value=scripts ), patch("custom_components.pyscript.global_ctx.open", mock_open(read_data=next_source)), patch( "custom_components.pyscript.open", mock_open(read_data=next_source) ), patch( "custom_components.pyscript.trigger.dt_now", return_value=now ), patch( "homeassistant.config.load_yaml_config_file", return_value={} ), patch( "custom_components.pyscript.os.path.getmtime", return_value=1000 ), patch( "custom_components.pyscript.global_ctx.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.install_requirements", return_value=None, ): reload_param = {} if i % 2 == 1: # # on alternate times, just reload the specific file we are testing with # reload_param = {"global_ctx": "file.hello"} await hass.services.async_call("pyscript", "reload", reload_param, blocking=True) if i % 3 == 0: # # reload a file that doesn't exist; will log error and do nothing # await hass.services.async_call( "pyscript", "reload", {"global_ctx": "file.nosuchfile"}, blocking=True) assert "pyscript.reload: no global context 'file.nosuchfile' to reload" in caplog.text
async def test_service_description(hass): """Test service description defined in doc_string.""" await setup_script( hass, None, dt(2020, 7, 1, 11, 59, 59, 999999), """ @service def func_no_doc_string(param1=None): pass @service def func_simple_doc_string(param2=None, param3=None): \"\"\"This is func2_simple_doc_string.\"\"\" pass @service def func_yaml_doc_string(param2=None, param3=None): \"\"\"yaml description: This is func_yaml_doc_string. fields: param1: description: first argument example: 12 param2: description: second argument example: 34 \"\"\" pass """, ) integration = loader.Integration( hass, "custom_components.pyscript", pathlib.Path("custom_components/pyscript"), { "name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation" }, ) with patch( "homeassistant.loader.async_get_custom_components", return_value={"pyscript": integration}, ): descriptions = (await async_get_all_descriptions(hass))[DOMAIN] assert descriptions["func_no_doc_string"][ "description"] == "pyscript function func_no_doc_string()" assert descriptions["func_no_doc_string"]["fields"] == { "param1": { "description": "argument param1" } } assert descriptions["func_simple_doc_string"][ "description"] == "This is func2_simple_doc_string." assert descriptions["func_simple_doc_string"]["fields"] == { "param2": { "description": "argument param2" }, "param3": { "description": "argument param3" }, } assert descriptions["func_yaml_doc_string"][ "description"] == "This is func_yaml_doc_string." assert descriptions["func_yaml_doc_string"]["fields"] == { "param1": { "description": "first argument", "example": "12" }, "param2": { "description": "second argument", "example": "34" }, }
async def setup_script(hass, now, source, no_connect=False): """Initialize and load the given pyscript.""" conf_dir = hass.config.path(FOLDER) file_contents = {f"{conf_dir}/hello.py": source} mock_open = MockOpen() for key, value in file_contents.items(): mock_open[key].read_data = value def isfile_side_effect(arg): return arg in file_contents def glob_side_effect(path, recursive=None): result = [] path_re = path.replace("*", "[^/]*").replace(".", "\\.") path_re = path_re.replace("[^/]*[^/]*/", ".*") for this_path in file_contents: if re.match(path_re, this_path): result.append(this_path) return result with patch( "custom_components.pyscript.os.path.isdir", return_value=True ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch( "custom_components.pyscript.global_ctx.open", mock_open ), patch( "custom_components.pyscript.trigger.dt_now", return_value=now ), patch("custom_components.pyscript.open", mock_open), patch( "homeassistant.config.load_yaml_config_file", return_value={} ), patch( "custom_components.pyscript.install_requirements", return_value=None, ), patch("custom_components.pyscript.watchdog_start", return_value=None), patch( "custom_components.pyscript.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.global_ctx.os.path.getmtime", return_value=1000), patch( "custom_components.pyscript.os.path.isfile" ) as mock_isfile: mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect assert await async_setup_component(hass, "pyscript", {DOMAIN: {}}) # # I'm not sure how to run the mock all the time, so just force the dt_now() # trigger function to return the given list of times in now. # def return_next_time(): nonlocal now if isinstance(now, list): if len(now) > 1: return now.pop(0) return now[0] return now trigger.__dict__["dt_now"] = return_next_time kernel_state_var = "pyscript.jupyter_ports_1234" kernel_cfg = { "ip": "127.0.0.1", "key": SECRET_KEY.decode("utf-8"), "signature_scheme": "hmac-sha256", "state_var": kernel_state_var, } if no_connect: kernel_cfg["no_connect_timeout"] = 0.0 await hass.services.async_call("pyscript", "jupyter_kernel_start", kernel_cfg) while True: ports_state = hass.states.get(kernel_state_var) if ports_state is not None: break await asyncio.sleep(2e-3) port_nums = json.loads(ports_state.state) sock = {} if no_connect: return sock, port_nums for name in PORT_NAMES: kernel_reader, kernel_writer = await asyncio.open_connection( "127.0.0.1", port_nums[name]) sock[name] = ZmqSocket(kernel_reader, kernel_writer, "ROUTER") await sock[name].handshake() return sock, port_nums
def pyscript_bypass_setup_fixture(): """Mock component setup.""" with patch("custom_components.pyscript.async_setup_entry", return_value=True): yield
async def test_service_call(hass): """Test calling a service using the entity_id as a property.""" with patch( "custom_components.pyscript.state.async_get_all_descriptions", return_value={ "test": { "test": { "description": None, "fields": { "entity_id": "blah", "other_service_data": "blah" } } } }, ), patch.object(hass.states, "get", return_value=HassState("test.entity", "True")), patch.object( hass.services, "async_call") as call: State.init(hass) await State.get_service_params() func = await State.get("test.entity.test") await func(context=Context(id="test"), blocking=True, limit=1, other_service_data="test") assert call.called assert call.call_args[0] == ( "test", "test", { "other_service_data": "test", "entity_id": "test.entity" }, ) assert call.call_args[1] == { "context": Context(id="test"), "blocking": True, "limit": 1 } call.reset_mock() func = await State.get("test.entity.test") await func(context=Context(id="test"), blocking=False, other_service_data="test") assert call.called assert call.call_args[0] == ( "test", "test", { "other_service_data": "test", "entity_id": "test.entity" }, ) assert call.call_args[1] == { "context": Context(id="test"), "blocking": False }