• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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