def test_on_dir_raises_error_with_all_incorrect_hars( self, tmp_path: Path): for i in range(2): tmp_path.joinpath(f"{i}.nothar").write_text("not JSON!") with pytest.raises(SkippableScenarioError): Scenario.from_path(tmp_path)
def test_generates_passed_global_code_blocks(): def mock(name, blocks=None): m = MagicMock(spec=Scenario, children=[], global_code_blocks=blocks or {}) # https://docs.python.org/3/library/unittest.mock.html#mock-names-and-the-name-attribute m.name = name return m sg1 = Scenario( "sg1", children=[ mock("a", blocks={"b1": ["ab"]}), mock("b", blocks={"b2": ["cd"]}) ], origin=None, ) sg2 = Scenario("sg2", children=[mock("c")], origin=None) sg3 = Scenario( "sg3", children=[mock("d", blocks={ "b3": ["yz"], "b2": ["yyy", "zzz"] })], origin=None, ) code = locustfile([sg1, sg2, sg3]) assert code.endswith("\n# b1\nab\n# b2\nyyy\nzzz\n# b3\nyz" ), "the latter b2 block should override the former"
def test_it_renders_a_locustfile_template(self): a_name = "some_task" a_request = MagicMock(spec=Request) a_request.method = HttpMethod.GET a_request.url = MagicMock(spec=SplitResult) a_request.url.scheme = "some_scheme" a_request.url.hostname = "some_hostname" a_request.url.path = "some_path" a_request.url.geturl() a_request.url.geturl.return_value = "some_url" a_request.headers = {"a": "b"} a_request.name = None task = Task(a_name, a_request) scenario = Scenario(name="SomeScenario", children=[task], origin=None) scenario_group = Scenario(name="ScenarioGroup", children=[scenario], weight=2, origin=None) script = locustfile([scenario_group]) expected = string.Template(f""" # File automatically generated by Transformer v{__version__}: # https://github.com/zalando-incubator/Transformer import re from distutils.version import LooseVersion from locust import __version__ LOCUST_MAJOR_VERSION = LooseVersion(__version__).version[0] if LOCUST_MAJOR_VERSION >= 1: from locust import HttpUser from locust import SequentialTaskSet from locust import TaskSet from locust import task HttpLocust = HttpUser TaskSequence = SequentialTaskSet def seq_task(_): return task else: from locust import HttpLocust from locust import TaskSequence from locust import TaskSet from locust import seq_task from locust import task class ScenarioGroup(TaskSet): @task(1) class SomeScenario(TaskSequence): @seq_task(1) def some_task(self): response = self.client.get(url='some_url', name='some_url', timeout=$TIMEOUT, allow_redirects=False, headers={{'a': 'b'}}) class LocustForScenarioGroup(HttpLocust): if LOCUST_MAJOR_VERSION >= 1: tasks = [ScenarioGroup] else: task_set = ScenarioGroup weight = 2 min_wait = 0 max_wait = 10 """).safe_substitute({"TIMEOUT": TIMEOUT}) assert expected.strip() == script.strip()
def test_group_records_global_code_blocks_from_scenarios(self): t1_blocks = {"t1-1": ["abc"], "t1-2": ["def"]} t1 = Task("t1", request=MagicMock(), global_code_blocks=t1_blocks) t2 = Task("t2", request=MagicMock()) t3_blocks = {"t3-1": ("xyz",)} t3 = Task("t3", request=MagicMock(), global_code_blocks=t3_blocks) s1 = Scenario("s1", [t1, t2], origin=None) s2 = Scenario("s2", [t3], origin=None) sg = Scenario("sg", [s1, s2], origin=None) assert sg.global_code_blocks == {**t1_blocks, **t3_blocks}
def test_on_dir_with_dangling_weights_raises_error( self, tmp_path: Path, dummy_har_string, caplog): (tmp_path / "ok.har").write_text(dummy_har_string) (tmp_path / "fail.weight").write_text("7") caplog.set_level(logging.INFO) with pytest.raises(SkippableScenarioError): Scenario.from_path(tmp_path) assert "weight file" in caplog.text assert any(r.levelname == "ERROR" for r in caplog.records), "at least one ERROR logged"
def test_raises_error_for_colliding_scenario_names_from_har_files( self, tmp_path: Path, dummy_har_string, caplog): (tmp_path / "good.har").write_text(dummy_har_string) (tmp_path / "bad.har").write_text(dummy_har_string) (tmp_path / "bad.json").write_text(dummy_har_string) caplog.set_level(logging.ERROR) with pytest.raises(SkippableScenarioError): Scenario.from_path(tmp_path) assert "colliding names" in caplog.text assert "bad.har" in caplog.text assert "bad.json" in caplog.text
def test_uses_full_path_for_scenario_name(self, tmp_path: Path, dummy_har_string): har_basename = "e3ee4a1ef0817cde0a0a78c056e7cb35" har_path = tmp_path / har_basename har_path.write_text(dummy_har_string) scenario = Scenario.from_path(har_path) words_in_scenario_name = { m.group() for m in re.finditer(r"[A-Za-z0-9]+", scenario.name) } assert har_basename in words_in_scenario_name words_in_parent_path = { m.group() for m in re.finditer(r"[A-Za-z0-9]+", str(tmp_path)) } words_in_scenario_name_not_from_har_basename = words_in_scenario_name - { har_basename } assert ( words_in_parent_path <= words_in_scenario_name_not_from_har_basename ), "all components of the parent path must be in the scenario name"
def test_with_weight_file_has_corresponding_weight(self, tmp_path: Path): weight_path = tmp_path / "test.weight" weight_path.write_text("74") har_path = tmp_path / "test.har" har_path.write_text(DUMMY_HAR_STRING) assert Scenario.from_path(har_path).weight == 74
def test_with_invalid_weight_raises_error_and_never_skips( self, tmp_path: Path, dummy_har_string, weight): legit_har_path = tmp_path / "legit.har" legit_har_path.write_text(dummy_har_string) bad_weight_path = tmp_path / "test.weight" bad_weight_path.write_text(str(weight)) bad_weight_har_path = tmp_path / "test.har" bad_weight_har_path.write_text(dummy_har_string) with pytest.raises(WeightValueError): # If from_path was skipping the bad scenario/weight pair, it # would not raise because there is another valid scenario, # legit.har. Scenario.from_path(tmp_path)
def test_uses_full_path_for_parents_and_basename_for_children( self, tmp_path: Path ): root_basename = "615010a656a5bb29d1898f163619611f" root = tmp_path / root_basename root.mkdir() for i in range(2): (root / f"s{i}.har").write_text(DUMMY_HAR_STRING) root_scenario = Scenario.from_path(root) words_in_root_scenario_name = { m.group() for m in re.finditer(r"[A-Za-z0-9]+", root_scenario.name) } words_in_root_path = { m.group() for m in re.finditer(r"[A-Za-z0-9]+", str(root)) } assert ( words_in_root_path <= words_in_root_scenario_name ), "parent scenario's name must come from full path" assert len(root_scenario.children) == 2 child_scenario_names = {c.name for c in root_scenario.children} assert child_scenario_names == { "s0", "s1", }, "child scenarios have short names"
def test_with_weight_file_has_corresponding_weight( self, tmp_path: Path, dummy_har_string): weight_path = tmp_path / "test.weight" weight_path.write_text("74") har_path = tmp_path / "test.har" har_path.write_text(dummy_har_string) assert Scenario.from_path(har_path).weight == 74
def test_records_global_code_blocks_from_tasks(self): t1_blocks = {"t1-1": ["abc"], "t1-2": ["def"]} t1 = Task("t1", request=MagicMock(), global_code_blocks=t1_blocks) t2 = Task("t2", request=MagicMock()) t3_blocks = {"t3-1": ("xyz",)} t3 = Task("t3", request=MagicMock(), global_code_blocks=t3_blocks) scenario = Scenario("scenario", [t1, t2, t3], origin=None) assert scenario.global_code_blocks == {**t1_blocks, **t3_blocks}
def test_raises_error_for_colliding_scenario_names_from_directory_and_file( self, tmp_path: Path, dummy_har_string, caplog): directory = tmp_path / "x" directory.mkdir() # directory needs to contain a HAR file, otherwise Transformer will # not consider it a scenario. (directory / "a.har").write_text(dummy_har_string) (tmp_path / "x.har").write_text(dummy_har_string) caplog.set_level(logging.ERROR) with pytest.raises(SkippableScenarioError): Scenario.from_path(tmp_path) assert "colliding names" in caplog.text assert re.search(r"\bx\b", caplog.text) assert re.search(r"\bx.har\b", caplog.text)
def test_on_dir_ignores_some_incorrect_hars(self, tmp_path: Path): not_har_path = tmp_path / "not.har" not_har_path.write_text("not JSON!") har_path = tmp_path / "good.har" har_path.write_text(DUMMY_HAR_STRING) scenario = Scenario.from_path(tmp_path) assert len(scenario.children) == 1 assert scenario.children[0].origin == har_path
def test_group_records_global_code_blocks_uniquely(self): common_blocks = {"x": ["a", "b"]} t1 = Task( "t1", request=MagicMock(), global_code_blocks={**common_blocks, "t1b": ["uvw"]}, ) t2 = Task( "t2", request=MagicMock(), global_code_blocks={**common_blocks, "t2b": ["xyz"]}, ) s1 = Scenario("s1", [t1], origin=None) s2 = Scenario("s2", [t2], origin=None) sg = Scenario("sg", [s1, s2], origin=None) assert sg.global_code_blocks == { **common_blocks, "t1b": ["uvw"], "t2b": ["xyz"], }
def test_creation_from_scenario_directory_with_weight_file(self, tmp_path: Path): root_path = tmp_path / "some-path" root_path.mkdir() expected_weight = 7 root_path.with_suffix(".weight").write_text(str(expected_weight)) nb_har_files = 2 for i in range(nb_har_files): root_path.joinpath(f"{i}.har").write_text(DUMMY_HAR_STRING) result = Scenario.from_path(root_path) assert len(result.children) == nb_har_files assert result.weight == expected_weight
def test_it_renders_a_locustfile_template(self): a_name = "some_task" a_request = MagicMock() a_request.method = HttpMethod.GET a_request.url.scheme = "some_scheme" a_request.url.hostname = "some_hostname" a_request.url.path = "some_path" a_request.url.geturl() a_request.url.geturl.return_value = "some_url" task = Task(a_name, a_request) scenario = Scenario(name="SomeScenario", children=[task], origin=None) scenario_group = Scenario(name="ScenarioGroup", children=[scenario], weight=2, origin=None) script = locustfile([scenario_group]) expected = string.Template(""" # File automatically generated by Transformer: # https://github.bus.zalan.do/TIP/transformer import re from locust import HttpLocust from locust import TaskSequence from locust import TaskSet from locust import seq_task from locust import task class ScenarioGroup(TaskSet): @task(1) class SomeScenario(TaskSequence): @seq_task(1) def some_task(self): response = self.client.get(url='some_url', name='some_url', headers={}, timeout=$TIMEOUT, allow_redirects=False) class LocustForScenarioGroup(HttpLocust): task_set = ScenarioGroup weight = 2 min_wait = 0 max_wait = 10 """).safe_substitute({"TIMEOUT": TIMEOUT}) assert expected.strip() == script.strip()
def test_generates_passed_global_code_blocks(): sg1 = Scenario( "sg1", children=[ MagicMock(spec_set=Scenario, children=[], global_code_blocks={"b1": ["ab"]}), MagicMock(spec_set=Scenario, children=[], global_code_blocks={"b2": ["cd"]}), ], origin=None, ) sg2 = Scenario( "sg2", children=[ MagicMock(spec_set=Scenario, children=[], global_code_blocks={}) ], origin=None, ) sg3 = Scenario( "sg3", children=[ MagicMock( spec_set=Scenario, children=[], global_code_blocks={ "b3": ["yz"], "b2": ["yyy", "zzz"] }, ) ], origin=None, ) code = locustfile([sg1, sg2, sg3]) assert code.endswith("\n# b1\nab\n# b2\nyyy\nzzz\n# b3\nyz" ), "the latter b2 block should override the former"
def test_with_many_weight_files_selects_weight_based_on_name( self, tmp_path: Path, dummy_har_string): expected_weight_path = tmp_path / "test.weight" expected_weight_path.write_text("7") first_wrong_weight_path = tmp_path / "a.weight" first_wrong_weight_path.write_text("2") second_wrong_weight_path = tmp_path / "1.weight" second_wrong_weight_path.write_text("4") har_path = tmp_path / "test.har" har_path.write_text(dummy_har_string) assert Scenario.from_path(har_path).weight == 7
def transform( scenarios_path: Union[str, Path], plugins: Sequence[Plugin] = (), with_default_plugins: bool = True, ) -> str: """ This function is deprecated and will be removed in a future version. Do not rely on it. Reason: It only accepts one scenario path at a time, and requires plugins to be already resolved (and therefore that users use transformer.plugins.resolve, which is kind of low-level). Both dumps & dump lift these constraints and have a more familiar naming (see json.dump/s, etc.). Deprecated since: v1.0.2. """ warnings.warn(DeprecationWarning("transform: use dump or dumps instead")) if with_default_plugins: plugins = (*DEFAULT_PLUGINS, *plugins) return locustfile([Scenario.from_path(Path(scenarios_path), plugins)])
def _dump_as_lines( scenario_paths: Iterable[LaxPath], plugins: Sequence[PluginName], with_default_plugins: bool, ) -> Iterator[str]: plugins = [p for name in plugins for p in plug.resolve(name)] if with_default_plugins: plugins = (*DEFAULT_PLUGINS, *plugins) plugins_for = plug.group_by_contract(plugins) scenarios = [ Scenario.from_path(path, plugins_for[Contract.OnTask], plugins_for[Contract.OnTaskSequence]).apply_plugins( plugins_for[Contract.OnScenario]) for path in scenario_paths ] yield from locustfile_lines(scenarios, plugins_for[Contract.OnPythonProgram])
def test_locust_taskset_raises_on_malformed_scenario(): bad_child = cast(Scenario, 7) bad_scenario = Scenario(name="x", children=[bad_child], origin=None) with pytest.raises(TypeError, match=r"unexpected type .*\bchildren"): locust_taskset(bad_scenario)
def test_on_har_raises_error_with_incorrect_har(self, tmp_path: Path): not_har_path = tmp_path / "not.har" not_har_path.write_text("not JSON!") with pytest.raises(SkippableScenarioError): Scenario.from_path(not_har_path)
def test_names_are_unique(*_, paths: List[Path]): scenario_names = [Scenario.from_path(path).name for path in paths] assert sorted(set(scenario_names)) == sorted(scenario_names) assert len(paths) == len(scenario_names)
def test_without_weight_file_has_weight_1(self, tmp_path: Path, dummy_har_string): har_path = tmp_path / "test.har" har_path.write_text(dummy_har_string) assert Scenario.from_path(har_path).weight == 1
def test_without_weight_file_has_weight_1(self, tmp_path: Path): har_path = tmp_path / "test.har" har_path.write_text(DUMMY_HAR_STRING) assert Scenario.from_path(har_path).weight == 1