1# Copyright 2022 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5# pytype: disable=attribute-error 6 7from __future__ import annotations 8 9import argparse 10import datetime as dt 11import json 12import pathlib 13import re 14import unittest 15from typing import List, Sequence, cast 16from unittest import mock 17 18from crossbench.action_runner.action.action_type import ActionType 19from crossbench.action_runner.base import ActionRunner 20from crossbench.action_runner.basic_action_runner import BasicActionRunner 21from crossbench.benchmarks.loading.config.blocks import ActionBlockListConfig 22from crossbench.benchmarks.loading.config.login.google import GOOGLE_LOGIN_URL 23from crossbench.benchmarks.loading.loading_benchmark import (LoadingPageFilter, 24 PageLoadBenchmark) 25from crossbench.benchmarks.loading.page.combined import CombinedPage 26from crossbench.benchmarks.loading.page.interactive import InteractivePage 27from crossbench.benchmarks.loading.page.live import (PAGE_LIST, PAGE_LIST_SMALL, 28 LivePage) 29from crossbench.benchmarks.loading.playback_controller import \ 30 PlaybackController 31from crossbench.benchmarks.loading.tab_controller import TabController 32from crossbench.browsers.settings import Settings 33from crossbench.cli.config.secrets import SecretsConfig 34from crossbench.env import HostEnvironmentConfig, ValidationMode 35from crossbench.runner.runner import Runner 36from tests import test_helper 37from tests.crossbench.base import BaseCliTestCase 38from tests.crossbench.benchmarks import helper 39from tests.crossbench.mock_browser import JsInvocation 40 41 42class TestPageLoadBenchmark(helper.SubStoryTestCase): 43 44 @property 45 def benchmark_cls(self): 46 return PageLoadBenchmark 47 48 def story_filter( # pylint: disable=arguments-differ 49 self, 50 patterns: Sequence[str], 51 separate: bool = True, 52 playback: PlaybackController = PlaybackController.default(), 53 tabs: TabController = TabController.default(), 54 action_runner: ActionRunner = BasicActionRunner(), 55 about_blank_duration: dt.timedelta = dt.timedelta(), 56 run_login: bool = True, 57 run_setup: bool = True) -> LoadingPageFilter: 58 args = argparse.Namespace( 59 about_blank_duration=about_blank_duration, 60 playback=playback, 61 tabs=tabs, 62 action_runner=action_runner, 63 run_login=run_login, 64 run_setup=run_setup) 65 return cast(LoadingPageFilter, 66 super().story_filter(patterns, args=args, separate=separate)) 67 68 def test_page_list(self): 69 self.assertTrue(PAGE_LIST) 70 self.assertTrue(PAGE_LIST_SMALL) 71 for page in PAGE_LIST: 72 self.assertIsInstance(page, InteractivePage) 73 for page in PAGE_LIST_SMALL: 74 self.assertIsInstance(page, InteractivePage) 75 76 def test_all_stories(self): 77 stories = self.story_filter(["all"]).stories 78 self.assertGreater(len(stories), 1) 79 for story in stories: 80 self.assertIsInstance(story, LivePage) 81 names = set(story.name for story in stories) 82 self.assertEqual(len(names), len(stories)) 83 self.assertEqual(names, set(page.name for page in PAGE_LIST)) 84 85 def test_default_stories(self): 86 stories = self.story_filter(["default"]).stories 87 self.assertGreater(len(stories), 1) 88 for story in stories: 89 self.assertIsInstance(story, LivePage) 90 names = set(story.name for story in stories) 91 self.assertEqual(len(names), len(stories)) 92 self.assertEqual(names, set(page.name for page in PAGE_LIST_SMALL)) 93 94 def test_combined_stories(self): 95 stories = self.story_filter(["all"], separate=False).stories 96 self.assertEqual(len(stories), 1) 97 combined = stories[0] 98 self.assertIsInstance(combined, CombinedPage) 99 100 def test_filter_by_name(self): 101 for preset_page in PAGE_LIST: 102 stories = self.story_filter([preset_page.name]).stories 103 self.assertListEqual([p.url for p in stories], [preset_page.url]) 104 with self.assertRaises(argparse.ArgumentTypeError) as cm: 105 self.story_filter([]) 106 self.assertIn("empty", str(cm.exception).lower()) 107 108 def test_filter_by_name_with_duration(self): 109 pages = PAGE_LIST 110 filtered_pages = self.story_filter([pages[0].name, pages[1].name, 111 "1001"]).stories 112 self.assertListEqual([p.url for p in filtered_pages], 113 [pages[0].url, pages[1].url]) 114 self.assertEqual(filtered_pages[0].duration, pages[0].duration) 115 self.assertEqual(filtered_pages[1].duration, dt.timedelta(seconds=1001)) 116 117 def test_page_by_url(self): 118 url1 = "http://example.com/test1" 119 url2 = "http://example.com/test2" 120 stories = self.story_filter([url1, url2]).stories 121 self.assertEqual(len(stories), 2) 122 self.assertEqual(stories[0].first_url, url1) 123 self.assertEqual(stories[1].first_url, url2) 124 125 def test_page_by_url_www(self): 126 url1 = "www.example.com/test1" 127 url2 = "www.example.com/test2" 128 stories = self.story_filter([url1, url2]).stories 129 self.assertEqual(len(stories), 2) 130 self.assertEqual(stories[0].first_url, f"https://{url1}") 131 self.assertEqual(stories[1].first_url, f"https://{url2}") 132 133 def test_page_by_url_combined(self): 134 url1 = "http://example.com/test1" 135 url2 = "http://example.com/test2" 136 stories = self.story_filter([url1, url2], separate=False).stories 137 self.assertEqual(len(stories), 1) 138 combined = stories[0] 139 self.assertIsInstance(combined, CombinedPage) 140 141 def test_run_combined(self): 142 stories = [CombinedPage(PAGE_LIST)] 143 self._test_run(stories) 144 self._assert_urls_loaded([story.url for story in PAGE_LIST]) 145 146 def test_run_default(self): 147 stories = PAGE_LIST 148 self._test_run(stories) 149 self._assert_urls_loaded([story.url for story in stories]) 150 151 def test_run_throw(self): 152 stories = PAGE_LIST 153 self._test_run(stories) 154 self._assert_urls_loaded([story.url for story in stories]) 155 156 def test_run_repeat_with_about_blank(self): 157 url1 = "https://www.example.com/test1" 158 url2 = "https://www.example.com/test2" 159 stories = self.story_filter( 160 [url1, url2], 161 separate=False, 162 about_blank_duration=dt.timedelta(seconds=1)).stories 163 self._test_run(stories) 164 urls = [url1, "about:blank", url2, "about:blank"] 165 self._assert_urls_loaded(urls) 166 167 def test_run_repeat_with_about_blank_separate(self): 168 url1 = "https://www.example.com/test1" 169 url2 = "https://www.example.com/test2" 170 stories = self.story_filter( 171 [url1, url2], 172 separate=True, 173 about_blank_duration=dt.timedelta(seconds=1)).stories 174 self._test_run(stories) 175 urls = [url1, "about:blank", url2, "about:blank"] 176 self._assert_urls_loaded(urls) 177 178 def test_run_repeat(self): 179 url1 = "https://www.example.com/test1" 180 url2 = "https://www.example.com/test2" 181 stories = self.story_filter([url1, url2], 182 separate=False, 183 playback=PlaybackController.repeat(3)).stories 184 self._test_run(stories) 185 urls = [url1, url2] * 3 186 self._assert_urls_loaded(urls) 187 188 def test_run_repeat_separate(self): 189 url1 = "https://www.example.com/test1" 190 url2 = "https://www.example.com/test2" 191 stories = self.story_filter([url1, url2], 192 separate=True, 193 playback=PlaybackController.repeat(3)).stories 194 self._test_run(stories) 195 urls = [url1] * 3 + [url2] * 3 196 self._assert_urls_loaded(urls) 197 198 def _test_run(self, stories, throw: bool = False): 199 benchmark = self.benchmark_cls(stories) 200 self.assertTrue(len(benchmark.describe()) > 0) 201 runner = Runner( 202 self.out_dir, 203 self.browsers, 204 benchmark, 205 env_config=HostEnvironmentConfig(), 206 env_validation_mode=ValidationMode.SKIP, 207 platform=self.platform, 208 throw=throw) 209 runner.run() 210 self.assertTrue(runner.is_success) 211 self.assertTrue(self.browsers[0].did_run) 212 self.assertTrue(self.browsers[1].did_run) 213 214 def _assert_urls_loaded(self, story_urls): 215 browser_1_urls = self.filter_splashscreen_urls(self.browsers[0].url_list) 216 self.assertEqual(browser_1_urls, story_urls) 217 browser_2_urls = self.filter_splashscreen_urls(self.browsers[1].url_list) 218 self.assertEqual(browser_2_urls, story_urls) 219 220 221class LoadingBenchmarkCliTestCase(BaseCliTestCase): 222 223 def test_invalid_duplicate_urls_stories(self): 224 with self.assertRaises(argparse.ArgumentTypeError) as cm: 225 with self.patch_get_browser(): 226 url = "http://test.com" 227 self.run_cli("loading", "run", f"--urls={url}", f"--stories={url}", 228 "--env-validation=skip", "--throw") 229 self.assertIn("--urls", str(cm.exception)) 230 self.assertIn("--stories", str(cm.exception)) 231 232 def test_invalid_duplicate_urls_config(self): 233 with self.assertRaises(argparse.ArgumentError) as cm: 234 with self.patch_get_browser(): 235 self.run_cli("loading", "run", "--urls=https://test.com", 236 "--page-config=config.hjson", "--env-validation=skip", 237 "--throw") 238 self.assertIn("--urls", str(cm.exception)) 239 self.assertIn("--page-config", str(cm.exception)) 240 241 def test_invalid_duplicate_stories_config(self): 242 with self.assertRaises(argparse.ArgumentTypeError) as cm: 243 with self.patch_get_browser(): 244 self.run_cli("loading", "run", "--stories=https://test.com", 245 "--page-config=config.hjson", "--env-validation=skip", 246 "--throw") 247 self.assertIn("--stories", str(cm.exception)) 248 self.assertIn("page config", str(cm.exception).lower()) 249 250 def test_conflicting_global_config(self): 251 config_data = { 252 "browsers": { 253 "chrome": "chrome-stable" 254 }, 255 "pages": { 256 "google_search_result": [{ 257 "action": "get", 258 "url": "https://www.google.com/search?q=cats" 259 },] 260 } 261 } 262 config_file = pathlib.Path("config.hjson") 263 with config_file.open("w", encoding="utf-8") as f: 264 json.dump(config_data, f) 265 with self.assertRaises(argparse.ArgumentTypeError) as cm: 266 with self.patch_get_browser(): 267 self.run_cli("loading", "run", "--stories=https://test.com", 268 "--config=config.hjson", "--page-config=config.hjson", 269 "--env-validation=skip", "--throw") 270 error_message = str(cm.exception).lower() 271 self.assertIn("conflict", error_message) 272 self.assertIn("--config", error_message) 273 self.assertIn("--page-config", error_message) 274 275 def test_page_list_file(self): 276 config = pathlib.Path("test/pages.txt") 277 self.fs.create_file(config) 278 url_1 = "http://one.test.com" 279 url_2 = "http://two.test.com" 280 with config.open("w", encoding="utf-8") as f: 281 f.write("\n".join((url_1, url_2))) 282 with self.patch_get_browser(): 283 self.run_cli("loading", "run", f"--urls-file={config}", 284 "--env-validation=skip", "--throw") 285 for browser in self.browsers: 286 self.assertListEqual([url_1, url_2], 287 browser.url_list[self.SPLASH_URLS_LEN:]) 288 289 def test_page_list_file_separate(self): 290 config = pathlib.Path("test/pages.txt") 291 self.fs.create_file(config) 292 url_1 = "http://one.test.com" 293 url_2 = "http://two.test.com" 294 with config.open("w", encoding="utf-8") as f: 295 f.write("\n".join((url_1, url_2))) 296 with self.patch_get_browser(): 297 self.run_cli("loading", "run", f"--urls-file={config}", 298 "--env-validation=skip", "--separate", "--throw") 299 for browser in self.browsers: 300 self.assertEqual(len(browser.url_list), (self.SPLASH_URLS_LEN + 1) * 2) 301 self.assertEqual(url_1, browser.url_list[self.SPLASH_URLS_LEN]) 302 self.assertEqual(url_2, browser.url_list[self.SPLASH_URLS_LEN * 2 + 1]) 303 304 def test_urls_single(self): 305 with self.patch_get_browser(): 306 url = "http://test.com" 307 self.run_cli("loading", "run", f"--urls={url}", "--env-validation=skip", 308 "--throw") 309 for browser in self.browsers: 310 self.assertListEqual([url], browser.url_list[self.SPLASH_URLS_LEN:]) 311 312 def test_urls_multiple(self): 313 with self.patch_get_browser(): 314 url_1 = "http://one.test.com" 315 url_2 = "http://two.test.com" 316 self.run_cli("loading", "run", f"--urls={url_1},{url_2}", 317 "--env-validation=skip", "--throw") 318 for browser in self.browsers: 319 self.assertListEqual([url_1, url_2], 320 browser.url_list[self.SPLASH_URLS_LEN:]) 321 322 def test_urls_multiple_separate(self): 323 with self.patch_get_browser(): 324 url_1 = "http://one.test.com" 325 url_2 = "http://two.test.com" 326 self.run_cli("loading", "run", f"--urls={url_1},{url_2}", 327 "--env-validation=skip", "--separate", "--throw") 328 for browser in self.browsers: 329 self.assertEqual(len(browser.url_list), (self.SPLASH_URLS_LEN + 1) * 2) 330 self.assertEqual(url_1, browser.url_list[self.SPLASH_URLS_LEN]) 331 self.assertEqual(url_2, browser.url_list[self.SPLASH_URLS_LEN * 2 + 1]) 332 333 def test_repeat_playback(self): 334 with self.patch_get_browser(): 335 url_1 = "http://one.test.com" 336 url_2 = "http://two.test.com" 337 self.run_cli("loading", "run", f"--urls={url_1},{url_2}", "--playback=2x", 338 "--env-validation=skip", "--throw") 339 for browser in self.browsers: 340 self.assertListEqual([url_1, url_2, url_1, url_2], 341 browser.url_list[self.SPLASH_URLS_LEN:]) 342 343 def test_repeat_playback_separate(self): 344 with self.patch_get_browser(): 345 url_1 = "http://one.test.com" 346 url_2 = "http://two.test.com" 347 self.run_cli("loading", "run", f"--urls={url_1},{url_2}", "--playback=2x", 348 "--separate", "--env-validation=skip", "--throw") 349 for browser in self.browsers: 350 self.assertEqual(len(browser.url_list), (self.SPLASH_URLS_LEN + 2) * 2) 351 self.assertListEqual( 352 [url_1, url_1], 353 browser.url_list[self.SPLASH_URLS_LEN:self.SPLASH_URLS_LEN + 2]) 354 self.assertListEqual([url_2, url_2], 355 browser.url_list[self.SPLASH_URLS_LEN * 2 + 2:]) 356 357 def simple_pages_config(self): 358 url_1 = "http://one.test.com" 359 url_2 = "http://two.test.com" 360 config = { 361 "pages": { 362 "test_one": [{ 363 "action": "get", 364 "url": url_1 365 }, { 366 "action": "get", 367 "url": url_2 368 }] 369 } 370 } 371 return url_1, url_2, config 372 373 def test_actions_config(self): 374 url_1, url_2, config = self.simple_pages_config() 375 config_file = pathlib.Path("test/page_config.json") 376 self.fs.create_file(config_file, contents=json.dumps(config)) 377 with self.patch_get_browser(): 378 self.run_cli("loading", "run", f"--page-config={config_file}", 379 "--env-validation=skip", "--throw") 380 for browser in self.browsers: 381 self.assertListEqual([url_1, url_2], 382 browser.url_list[self.SPLASH_URLS_LEN:]) 383 384 def setup_expected_google_login_js(self): 385 expected_scripts: List[JsInvocation] = [ 386 JsInvocation(True, re.compile(r".*Email or phone.*")), 387 JsInvocation(None, re.compile(r".*user@test.com.*")), 388 JsInvocation(True, re.compile(r".*passwordNext.*")), 389 JsInvocation(False, re.compile(r".*verifycontactNext.*")), 390 JsInvocation(True, re.compile(r".*Enter your password.*")), 391 JsInvocation(True, re.compile(r".*s3cr3t.*")), 392 JsInvocation(True, re.compile(r".*https://myaccount.google.com.*")), 393 ] 394 for browser in self.browsers: 395 for script in expected_scripts: 396 browser.expect_js(script) 397 398 def simple_pages_with_login_config(self): 399 url_1 = "http://one.test.com" 400 url_2 = "http://two.test.com" 401 config = { 402 "pages": { 403 "test_one": { 404 "login": 405 "google", 406 "actions": [{ 407 "action": "get", 408 "url": url_1 409 }, { 410 "action": "get", 411 "url": url_2 412 }] 413 } 414 } 415 } 416 return url_1, url_2, config 417 418 def test_actions_config_with_login_preset(self): 419 url_1, url_2, config = self.simple_pages_with_login_config() 420 config.update({ 421 "secrets": { 422 "google": { 423 "username": "user@test.com", 424 "password": "s3cr3t" 425 } 426 }, 427 }) 428 config_file = pathlib.Path("test/page_config.json") 429 self.fs.create_file(config_file, contents=json.dumps(config)) 430 self.setup_expected_google_login_js() 431 with self.patch_get_browser(): 432 self.run_cli("loading", "run", f"--page-config={config_file}", 433 "--env-validation=skip", "--throw") 434 for browser in self.browsers: 435 self.assertListEqual([GOOGLE_LOGIN_URL, url_1, url_2], 436 browser.url_list[self.SPLASH_URLS_LEN:]) 437 438 def test_actions_config_with_login_preset_global_secrets(self): 439 url_1, url_2, config = self.simple_pages_with_login_config() 440 config_file = pathlib.Path("test/page_config.json") 441 self.fs.create_file(config_file, contents=json.dumps(config)) 442 secrets_data = { 443 "google": { 444 "username": "user@test.com", 445 "password": "s3cr3t" 446 } 447 } 448 secrets_dict = SecretsConfig.parse(secrets_data).as_dict() 449 self.setup_expected_google_login_js() 450 with self.patch_get_browser(): 451 with mock.patch.object( 452 Settings, "secrets", 453 new_callable=mock.PropertyMock) as mock_get_secrets: 454 mock_get_secrets.return_value = secrets_dict 455 self.run_cli("loading", "run", f"--page-config={config_file}", 456 "--env-validation=skip", "--throw", 457 f"--secrets={json.dumps(secrets_data)}") 458 for browser in self.browsers: 459 self.assertListEqual([GOOGLE_LOGIN_URL, url_1, url_2], 460 browser.url_list[self.SPLASH_URLS_LEN:]) 461 462 def test_actions_config_with_login_preset_missing_secrets(self): 463 _, _, config = self.simple_pages_with_login_config() 464 config_file = pathlib.Path("test/page_config.json") 465 self.fs.create_file(config_file, contents=json.dumps(config)) 466 self.setup_expected_google_login_js() 467 with self.patch_get_browser(): 468 with self.assertRaises(Exception) as cm: 469 self.run_cli("loading", "run", f"--page-config={config_file}", 470 "--env-validation=skip", "--throw") 471 self.assertIn("google", str(cm.exception)) 472 473 def test_global_config_actions_config(self): 474 url_1 = "http://one.test.com" 475 url_2 = "http://two.test.com" 476 global_config_file = pathlib.Path("config.hjson") 477 global_config_data = { 478 # Dummy entry, not actually used by the test 479 "browsers": { 480 "chrome": "chrome-stable" 481 }, 482 "pages": { 483 "test_one": [{ 484 "action": "get", 485 "url": url_1 486 }, { 487 "action": "get", 488 "url": url_2 489 }] 490 } 491 } 492 with global_config_file.open("w", encoding="utf-8") as f: 493 json.dump(global_config_data, f) 494 with self.patch_get_browser(): 495 self.run_cli("loading", "run", f"--config={global_config_file}", 496 "--env-validation=skip", "--throw") 497 for browser in self.browsers: 498 self.assertListEqual([url_1, url_2], 499 browser.url_list[self.SPLASH_URLS_LEN:]) 500 501 502class ActionBlockListConfigTestCase(unittest.TestCase): 503 504 def test_parse_invalid(self): 505 for invalid in ("", (), {}, 1): 506 with self.subTest(invalid=invalid): 507 with self.assertRaises(argparse.ArgumentTypeError): 508 ActionBlockListConfig.parse(invalid) 509 510 def test_parse_default_action_list(self): 511 config = ActionBlockListConfig.parse([{ 512 "action": "get", 513 "url": "http://test.com", 514 "duration": "12.5s", 515 }]) 516 self.assertEqual(len(config.blocks), 1) 517 block = config.blocks[0] 518 self.assertEqual(block.label, "default") 519 self.assertEqual(len(block.actions), 1) 520 self.assertEqual(block.actions[0].TYPE, ActionType.GET) 521 self.assertEqual(block.duration, dt.timedelta(seconds=12.5)) 522 523 def test_parse_default_action_list_2(self): 524 config = ActionBlockListConfig.parse([{ 525 "action": "get", 526 "url": "http://test.com", 527 "duration": "12.5s", 528 }, { 529 "action": "wait", 530 "duration": "100s", 531 }]) 532 self.assertEqual(len(config.blocks), 1) 533 block = config.blocks[0] 534 self.assertEqual(block.label, "default") 535 self.assertEqual(len(block.actions), 2) 536 self.assertEqual(block.actions[0].TYPE, ActionType.GET) 537 self.assertEqual(block.actions[1].TYPE, ActionType.WAIT) 538 self.assertEqual(block.duration, dt.timedelta(seconds=112.5)) 539 540 def test_parse_single_block_action_list(self): 541 config = ActionBlockListConfig.parse([{ 542 "label": "block 1", 543 "actions": [{ 544 "action": "get", 545 "url": "http://test.com" 546 }] 547 }]) 548 self.assertEqual(len(config.blocks), 1) 549 block = config.blocks[0] 550 self.assertEqual(block.label, "block 1") 551 self.assertEqual(len(block.actions), 1) 552 self.assertEqual(block.actions[0].TYPE, ActionType.GET) 553 554 def test_parse_multi_block_action_list(self): 555 config = ActionBlockListConfig.parse([{ 556 "label": 557 "block 0", 558 "actions": [{ 559 "action": "get", 560 "url": "http://test.com/0", 561 "duration": "10s", 562 }] 563 }, { 564 "label": 565 "block 1", 566 "actions": [{ 567 "action": "get", 568 "url": "http://test.com/1", 569 "duration": "11s", 570 }] 571 }]) 572 self.assertEqual(len(config.blocks), 2) 573 for index, block in enumerate(config.blocks): 574 self.assertEqual(block.label, f"block {index}") 575 self.assertEqual(len(block.actions), 1) 576 self.assertEqual(block.actions[0].TYPE, ActionType.GET) 577 self.assertEqual(block.actions[0].url, f"http://test.com/{index}") 578 self.assertEqual(block.duration, dt.timedelta(seconds=10 + index)) 579 580 def test_parse_single_block_dict(self): 581 config = ActionBlockListConfig.parse( 582 {"block 1": { 583 "actions": [{ 584 "action": "get", 585 "url": "http://test.com" 586 }] 587 }}) 588 self.assertEqual(len(config.blocks), 1) 589 block = config.blocks[0] 590 self.assertEqual(block.label, "block 1") 591 self.assertEqual(len(block.actions), 1) 592 self.assertEqual(block.actions[0].TYPE, ActionType.GET) 593 594 def test_parse_block_dict_action_list_2(self): 595 config = ActionBlockListConfig.parse({ 596 "block 1": [{ 597 "action": "get", 598 "url": "http://test.com" 599 }, { 600 "action": "wait", 601 "duration": "2s" 602 }] 603 }) 604 self.assertEqual(len(config.blocks), 1) 605 block = config.blocks[0] 606 self.assertEqual(block.label, "block 1") 607 self.assertEqual(len(block.actions), 2) 608 self.assertEqual(block.actions[0].TYPE, ActionType.GET) 609 self.assertEqual(block.actions[1].TYPE, ActionType.WAIT) 610 611 def test_parse_single_block_multi_action_dict(self): 612 config = ActionBlockListConfig.parse({ 613 "block 1": { 614 "actions": [{ 615 "action": "get", 616 "url": "http://test.com/0", 617 "duration": "1s", 618 }, { 619 "action": "get", 620 "url": "http://test.com/1", 621 "duration": "20s", 622 }] 623 } 624 }) 625 self.assertEqual(len(config.blocks), 1) 626 block = config.blocks[0] 627 self.assertEqual(block.label, "block 1") 628 self.assertEqual(block.duration, dt.timedelta(seconds=21)) 629 self.assertEqual(len(block.actions), 2) 630 for index, action in enumerate(block.actions): 631 self.assertEqual(action.TYPE, ActionType.GET) 632 self.assertEqual(action.url, f"http://test.com/{index}") 633 634 def test_parse_multi_block_actions_dict(self): 635 config = ActionBlockListConfig.parse({ 636 "block 0": { 637 "actions": [{ 638 "action": "get", 639 "url": "http://test.com/0" 640 }] 641 }, 642 "block 1": { 643 "actions": [{ 644 "action": "get", 645 "url": "http://test.com/1" 646 }] 647 } 648 }) 649 self.assertEqual(len(config.blocks), 2) 650 for index, block in enumerate(config.blocks): 651 self.assertEqual(block.label, f"block {index}") 652 self.assertEqual(len(block.actions), 1) 653 self.assertEqual(block.actions[0].TYPE, ActionType.GET) 654 self.assertEqual(block.actions[0].url, f"http://test.com/{index}") 655 656 def test_parse_multi_block_actions_list(self): 657 config = ActionBlockListConfig.parse({ 658 "block 0": [{ 659 "action": "get", 660 "url": "http://test.com/0" 661 }], 662 "block 1": [{ 663 "action": "get", 664 "url": "http://test.com/1" 665 }] 666 }) 667 self.assertEqual(len(config.blocks), 2) 668 for index, block in enumerate(config.blocks): 669 self.assertEqual(block.label, f"block {index}") 670 self.assertEqual(len(block.actions), 1) 671 self.assertEqual(block.actions[0].TYPE, ActionType.GET) 672 self.assertEqual(block.actions[0].url, f"http://test.com/{index}") 673 674 def test_parse_dict_label_conflict(self): 675 with self.assertRaises(argparse.ArgumentTypeError) as cm: 676 ActionBlockListConfig.parse({ 677 "block 1": { 678 "label": "block 2", 679 "actions": [{ 680 "action": "get", 681 "url": "http://test.com" 682 }] 683 } 684 }) 685 self.assertIn("block 2", str(cm.exception)) 686 687 def test_parse_invalid_dict_missing_actions(self): 688 with self.assertRaises(argparse.ArgumentTypeError) as cm: 689 ActionBlockListConfig.parse({"block 1": {}}) 690 self.assertIn("actions", str(cm.exception)) 691 692 def test_parse_invalid_dict_empty_actions(self): 693 with self.assertRaises(argparse.ArgumentTypeError) as cm: 694 ActionBlockListConfig.parse({"block 1": {"actions": []}}) 695 self.assertIn("actions", str(cm.exception)) 696 697 def test_parse_logins(self): 698 with self.assertRaises(argparse.ArgumentTypeError) as cm: 699 _ = ActionBlockListConfig.parse({ 700 "login": [{ 701 "action": "get", 702 "url": "http://test.com/login" 703 }], 704 "block 0": [{ 705 "action": "get", 706 "url": "http://test.com/1" 707 }] 708 }) 709 self.assertIn("login", str(cm.exception)) 710 711 712if __name__ == "__main__": 713 test_helper.run_pytest(__file__) 714