summaryrefslogtreecommitdiffstats
path: root/tools/telemetry
diff options
context:
space:
mode:
authordtu@chromium.org <dtu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-11-08 08:17:22 +0000
committerdtu@chromium.org <dtu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-11-08 08:17:22 +0000
commiteea2e67034a223c74ce0e8c0517f6066cc1ee143 (patch)
tree64c4082a9d8e3c0df8fa64f689c7fe7a09b0edbb /tools/telemetry
parent5971ec68da0d265e7d87352dfc4cec23e65c4a2f (diff)
downloadchromium_src-eea2e67034a223c74ce0e8c0517f6066cc1ee143.zip
chromium_src-eea2e67034a223c74ce0e8c0517f6066cc1ee143.tar.gz
chromium_src-eea2e67034a223c74ce0e8c0517f6066cc1ee143.tar.bz2
[chrome_remote_control] Rename chrome_remote_control to telemetry.
TBR=nduca@chromium.org TBR=torne@chromium.org BUG=159613 TEST=./run_tests in tools/telemetry and tools/perf show no regressions. (They're not passing right now.) greps for "crc", "chrome_remote_control", and related terms return no results. Review URL: https://codereview.chromium.org/11361165 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@166638 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools/telemetry')
-rw-r--r--tools/telemetry/.gitignore4
-rw-r--r--tools/telemetry/OWNERS7
-rw-r--r--tools/telemetry/PRESUBMIT.py34
-rw-r--r--tools/telemetry/README5
-rw-r--r--tools/telemetry/examples/credentials_example.json10
-rwxr-xr-xtools/telemetry/examples/list_available_browsers21
-rwxr-xr-xtools/telemetry/examples/rendering_microbenchmark_test.py76
-rwxr-xr-xtools/telemetry/examples/telemetry_perf_test.py43
-rw-r--r--tools/telemetry/examples/top1k999
-rwxr-xr-xtools/telemetry/run_tests16
-rw-r--r--tools/telemetry/telemetry/__init__.py30
-rw-r--r--tools/telemetry/telemetry/adb_commands.py152
-rw-r--r--tools/telemetry/telemetry/android_browser_backend.py125
-rw-r--r--tools/telemetry/telemetry/android_browser_finder.py149
-rw-r--r--tools/telemetry/telemetry/android_browser_finder_unittest.py88
-rw-r--r--tools/telemetry/telemetry/android_platform.py28
-rw-r--r--tools/telemetry/telemetry/browser.py110
-rw-r--r--tools/telemetry/telemetry/browser_backend.py115
-rw-r--r--tools/telemetry/telemetry/browser_credentials.py131
-rw-r--r--tools/telemetry/telemetry/browser_credentials_unittest.py68
-rw-r--r--tools/telemetry/telemetry/browser_finder.py73
-rw-r--r--tools/telemetry/telemetry/browser_gone_exception.py9
-rw-r--r--tools/telemetry/telemetry/browser_options.py148
-rw-r--r--tools/telemetry/telemetry/browser_options_unittest.py60
-rw-r--r--tools/telemetry/telemetry/browser_unittest.py51
-rw-r--r--tools/telemetry/telemetry/cros_browser_backend.py180
-rw-r--r--tools/telemetry/telemetry/cros_browser_finder.py76
-rw-r--r--tools/telemetry/telemetry/cros_browser_finder_unittest.py11
-rw-r--r--tools/telemetry/telemetry/cros_interface.py375
-rw-r--r--tools/telemetry/telemetry/cros_interface_unittest.py111
-rw-r--r--tools/telemetry/telemetry/desktop_browser_backend.py110
-rw-r--r--tools/telemetry/telemetry/desktop_browser_finder.py151
-rw-r--r--tools/telemetry/telemetry/desktop_browser_finder_unittest.py188
-rw-r--r--tools/telemetry/telemetry/facebook_credentials_backend.py27
-rw-r--r--tools/telemetry/telemetry/form_based_credentials_backend.py92
-rw-r--r--tools/telemetry/telemetry/google_credentials_backend.py27
-rw-r--r--tools/telemetry/telemetry/google_credentials_backend_unittest.py67
-rw-r--r--tools/telemetry/telemetry/inspector_backend.py124
-rw-r--r--tools/telemetry/telemetry/inspector_console.py58
-rw-r--r--tools/telemetry/telemetry/inspector_console_unittest.py36
-rw-r--r--tools/telemetry/telemetry/inspector_page.py65
-rw-r--r--tools/telemetry/telemetry/inspector_page_unittest.py19
-rw-r--r--tools/telemetry/telemetry/inspector_runtime.py57
-rw-r--r--tools/telemetry/telemetry/inspector_runtime_unittest.py31
-rw-r--r--tools/telemetry/telemetry/multi_page_benchmark.py178
-rwxr-xr-xtools/telemetry/telemetry/multi_page_benchmark_runner.py106
-rw-r--r--tools/telemetry/telemetry/multi_page_benchmark_unittest.py92
-rw-r--r--tools/telemetry/telemetry/multi_page_benchmark_unittest_base.py49
-rw-r--r--tools/telemetry/telemetry/options_for_unittests.py28
-rw-r--r--tools/telemetry/telemetry/page.py28
-rw-r--r--tools/telemetry/telemetry/page_runner.py184
-rw-r--r--tools/telemetry/telemetry/page_runner_unittest.py91
-rw-r--r--tools/telemetry/telemetry/page_set.py57
-rw-r--r--tools/telemetry/telemetry/page_set_unittest.py28
-rw-r--r--tools/telemetry/telemetry/page_test.py50
-rw-r--r--tools/telemetry/telemetry/platform.py22
-rw-r--r--tools/telemetry/telemetry/possible_browser.py22
-rw-r--r--tools/telemetry/telemetry/run_tests.py143
-rw-r--r--tools/telemetry/telemetry/simple_mock.py98
-rw-r--r--tools/telemetry/telemetry/simple_mock_unittest.py84
-rw-r--r--tools/telemetry/telemetry/system_stub.py140
-rw-r--r--tools/telemetry/telemetry/tab.py77
-rw-r--r--tools/telemetry/telemetry/tab_crash_exception.py9
-rw-r--r--tools/telemetry/telemetry/tab_test_case.py29
-rw-r--r--tools/telemetry/telemetry/tab_unittest.py24
-rw-r--r--tools/telemetry/telemetry/temporary_http_server.py63
-rw-r--r--tools/telemetry/telemetry/temporary_http_server_unittest.py25
-rw-r--r--tools/telemetry/telemetry/util.py32
-rw-r--r--tools/telemetry/telemetry/util_unittest.py18
-rw-r--r--tools/telemetry/telemetry/websocket.py16
-rw-r--r--tools/telemetry/telemetry/websocket_unittest.py10
-rw-r--r--tools/telemetry/telemetry/wpr_modes.py7
-rw-r--r--tools/telemetry/telemetry/wpr_server.py47
-rw-r--r--tools/telemetry/third_party/websocket-client/.gitignore8
-rw-r--r--tools/telemetry/third_party/websocket-client/LICENSE506
-rw-r--r--tools/telemetry/third_party/websocket-client/README.chromium20
-rw-r--r--tools/telemetry/third_party/websocket-client/README.rst140
-rw-r--r--tools/telemetry/third_party/websocket-client/websocket.py756
-rw-r--r--tools/telemetry/unittest_data/.gitignore1
-rw-r--r--tools/telemetry/unittest_data/blank.html8
-rw-r--r--tools/telemetry/unittest_data/non_scrollable_page.html10
-rw-r--r--tools/telemetry/unittest_data/page_that_logs_to_console.html14
-rw-r--r--tools/telemetry/unittest_data/scrollable_page.html9
83 files changed, 7586 insertions, 0 deletions
diff --git a/tools/telemetry/.gitignore b/tools/telemetry/.gitignore
new file mode 100644
index 0000000..4269dc8
--- /dev/null
+++ b/tools/telemetry/.gitignore
@@ -0,0 +1,4 @@
+*.pyc
+\#*#
+.#*
+*.swp
diff --git a/tools/telemetry/OWNERS b/tools/telemetry/OWNERS
new file mode 100644
index 0000000..1cbe471
--- /dev/null
+++ b/tools/telemetry/OWNERS
@@ -0,0 +1,7 @@
+# The set noparent is temporary until src/OWNERS isn't *.
+set noparent
+alokp@chromium.org
+dtu@chromium.org
+nduca@chromium.org
+tonyg@chromium.org
+hartmanng@chromium.org
diff --git a/tools/telemetry/PRESUBMIT.py b/tools/telemetry/PRESUBMIT.py
new file mode 100644
index 0000000..6866cc2
--- /dev/null
+++ b/tools/telemetry/PRESUBMIT.py
@@ -0,0 +1,34 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import sys
+
+PYLINT_BLACKLIST = []
+PYLINT_DISABLED_WARNINGS = ['R0923', 'R0201', 'E1101']
+
+def _CommonChecks(input_api, output_api):
+ results = []
+ old_sys_path = sys.path
+ try:
+ sys.path = [os.path.join('..', 'telemetry')] + sys.path
+ results.extend(input_api.canned_checks.RunPylint(
+ input_api, output_api,
+ black_list=PYLINT_BLACKLIST,
+ disabled_warnings=PYLINT_DISABLED_WARNINGS))
+ finally:
+ sys.path = old_sys_path
+
+
+
+ return results
+
+def CheckChangeOnUpload(input_api, output_api):
+ report = []
+ report.extend(_CommonChecks(input_api, output_api))
+ return report
+
+def CheckChangeOnCommit(input_api, output_api):
+ report = []
+ report.extend(_CommonChecks(input_api, output_api))
+ return report
diff --git a/tools/telemetry/README b/tools/telemetry/README
new file mode 100644
index 0000000..744f53f
--- /dev/null
+++ b/tools/telemetry/README
@@ -0,0 +1,5 @@
+telemetry provides automation of chrome instances on top of the
+chrome developer tools protocol.
+
+The protocol we use:
+https://developers.google.com/chrome-developer-tools/docs/remote-debugging
diff --git a/tools/telemetry/examples/credentials_example.json b/tools/telemetry/examples/credentials_example.json
new file mode 100644
index 0000000..c7c53c5
--- /dev/null
+++ b/tools/telemetry/examples/credentials_example.json
@@ -0,0 +1,10 @@
+{
+ "google": {
+ "username": "<your google account here>",
+ "password": "<your google password here>"
+ }
+ "facebook": {
+ "username": "<your google account here>",
+ "password": "<your google password here>"
+ }
+}
diff --git a/tools/telemetry/examples/list_available_browsers b/tools/telemetry/examples/list_available_browsers
new file mode 100755
index 0000000..d40646c
--- /dev/null
+++ b/tools/telemetry/examples/list_available_browsers
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import sys
+
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import telemetry
+
+def Main(args):
+ options = telemetry.BrowserOptions()
+ options.browser_type = 'list';
+ parser = options.CreateParser('list_available_browsers')
+ parser.parse_args()
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv[1:]))
diff --git a/tools/telemetry/examples/rendering_microbenchmark_test.py b/tools/telemetry/examples/rendering_microbenchmark_test.py
new file mode 100755
index 0000000..9976c74
--- /dev/null
+++ b/tools/telemetry/examples/rendering_microbenchmark_test.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import re
+import sys
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import telemetry
+
+def Main(args):
+ options = telemetry.BrowserOptions()
+ parser = options.CreateParser('rendering_microbenchmark_test.py <sitelist>')
+ # TODO(nduca): Add test specific options here, if any.
+ options, args = parser.parse_args(args)
+ if len(args) != 1:
+ parser.print_usage()
+ return 255
+
+ urls = []
+ with open(args[0], 'r') as f:
+ for url in f.readlines():
+ url = url.strip()
+ if not re.match('(.+)://', url):
+ url = 'http://%s' % url
+ urls.append(url)
+
+ options.extra_browser_args.append('--enable-gpu-benchmarking')
+ browser_to_create = telemetry.FindBrowser(options)
+ if not browser_to_create:
+ sys.stderr.write('No browser found! Supported types: %s' %
+ telemetry.GetAllAvailableBrowserTypes(options))
+ return 255
+ with browser_to_create.Create() as b:
+ with b.ConnectToNthTab(0) as tab:
+ # Check browser for benchmark API. Can only be done on non-chrome URLs.
+ tab.page.Navigate('http://www.google.com')
+ import time
+ time.sleep(2)
+ tab.WaitForDocumentReadyStateToBeComplete()
+ if tab.runtime.Evaluate('window.chrome.gpuBenchmarking === undefined'):
+ print 'Browser does not support gpu benchmarks API.'
+ return 255
+
+ if tab.runtime.Evaluate(
+ 'window.chrome.gpuBenchmarking.runRenderingBenchmarks === undefined'):
+ print 'Browser does not support rendering benchmarks API.'
+ return 255
+
+ # Run the test. :)
+ first_line = []
+ def DumpResults(url, results):
+ if len(first_line) == 0:
+ cols = ['url']
+ for r in results:
+ cols.append(r['benchmark'])
+ print ','.join(cols)
+ first_line.append(0)
+ cols = [url]
+ for r in results:
+ cols.append(str(r['result']))
+ print ','.join(cols)
+
+ for u in urls:
+ tab.page.Navigate(u)
+ tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
+ results = tab.runtime.Evaluate(
+ 'window.chrome.gpuBenchmarking.runRenderingBenchmarks();')
+ DumpResults(url, results)
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv[1:]))
diff --git a/tools/telemetry/examples/telemetry_perf_test.py b/tools/telemetry/examples/telemetry_perf_test.py
new file mode 100755
index 0000000..de9aceb
--- /dev/null
+++ b/tools/telemetry/examples/telemetry_perf_test.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import sys
+import time
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import telemetry
+
+def Main(args):
+ options = telemetry.BrowserOptions()
+ parser = options.CreateParser('telemetry_perf_test.py')
+ options, args = parser.parse_args(args)
+
+ browser_to_create = telemetry.FindBrowser(options)
+ assert browser_to_create
+ with browser_to_create.Create() as b:
+ with b.ConnectToNthTab(0) as tab:
+
+ # Measure round-trip-time for evaluate
+ times = []
+ for i in range(1000):
+ start = time.time()
+ tab.runtime.Evaluate('%i * 2' % i)
+ times.append(time.time() - start)
+ N = float(len(times))
+ avg = sum(times, 0.0) / N
+ squared_diffs = [(t - avg) * (t - avg) for t in times]
+ stdev = sum(squared_diffs, 0.0) / (N - 1)
+ times.sort()
+ percentile_75 = times[int(0.75 * N)]
+
+ print "%s: avg=%f; stdev=%f; min=%f; 75th percentile = %f" % (
+ "Round trip time (seconds)",
+ avg, stdev, min(times), percentile_75)
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv[1:]))
diff --git a/tools/telemetry/examples/top1k b/tools/telemetry/examples/top1k
new file mode 100644
index 0000000..f32a38c
--- /dev/null
+++ b/tools/telemetry/examples/top1k
@@ -0,0 +1,999 @@
+youtube.com
+wikipedia.org
+live.com
+twitter.com
+baidu.com
+qq.com
+amazon.com
+blogspot.com
+yahoo.com
+linkedin.com
+taobao.com
+yahoo.co.jp
+sina.com.cn
+msn.com
+wordpress.com
+yandex.ru
+babylon.com
+ebay.com
+bing.com
+163.com
+weibo.com
+microsoft.com
+soso.com
+mail.ru
+tumblr.com
+vk.com
+sohu.com
+t.co
+craigslist.org
+apple.com
+pinterest.com
+paypal.com
+ask.com
+bbc.co.uk
+blogger.com
+avg.com
+xhamster.com
+imdb.com
+xvideos.com
+fc2.com
+tudou.com
+youku.com
+livejasmin.com
+flickr.com
+go.com
+ifeng.com
+conduit.com
+tmall.com
+zedo.com
+aol.com
+hao123.com
+odnoklassniki.ru
+pornhub.com
+cnn.com
+blogspot.in
+ebay.de
+adobe.com
+rakuten.co.jp
+mywebsearch.com
+huffingtonpost.com
+alibaba.com
+about.com
+thepiratebay.se
+chinaz.com
+amazon.de
+mediafire.com
+sogou.com
+360buy.com
+ebay.co.uk
+ameblo.jp
+espn.go.com
+godaddy.com
+uol.com.br
+adf.ly
+netflix.com
+alipay.com
+redtube.com
+london2012.org
+4shared.com
+amazon.co.jp
+stackoverflow.com
+doubleclick.com
+imgur.com
+youporn.com
+globo.com
+livedoor.com
+wordpress.org
+bp.blogspot.com
+instagram.com
+dailymotion.com
+wigetmedia.com
+cnet.com
+searchnu.com
+zeekrewards.com
+nytimes.com
+renren.com
+amazon.co.uk
+douban.com
+weather.com
+search-results.com
+adcash.com
+livejournal.com
+xnxx.com
+dailymail.co.uk
+torrentz.eu
+tianya.cn
+cnzz.com
+bankofamerica.com
+ehow.com
+badoo.com
+tube8.com
+vimeo.com
+deviantart.com
+addthis.com
+mozilla.org
+indiatimes.com
+reddit.com
+secureserver.net
+booking.com
+aweber.com
+goo.ne.jp
+pconline.com.cn
+warriorforum.com
+spiegel.de
+incredibar.com
+clicksor.com
+4dsply.com
+360.cn
+stumbleupon.com
+chase.com
+56.com
+dropbox.com
+blogfa.com
+kat.ph
+answers.com
+pengyou.com
+outbrain.com
+wikia.com
+softonic.com
+yieldmanager.com
+sourceforge.net
+walmart.com
+photobucket.com
+comcast.net
+foxnews.com
+amazonaws.com
+onet.pl
+naver.com
+58.com
+wellsfargo.com
+guardian.co.uk
+wikimedia.org
+fbcdn.net
+xunlei.com
+skype.com
+myspace.com
+salesforce.com
+etsy.com
+depositfiles.com
+justbeenpaid.com
+liveinternet.ru
+statcounter.com
+mgid.com
+iqiyi.com
+adultfriendfinder.com
+bild.de
+filestube.com
+allegro.pl
+rapidshare.com
+reference.com
+fiverr.com
+rediff.com
+download.com
+optmd.com
+domaintools.com
+xinhuanet.com
+squidoo.com
+ucoz.ru
+leboncoin.fr
+zol.com.cn
+youjizz.com
+yelp.com
+slideshare.net
+free.fr
+files.wordpress.com
+hootsuite.com
+people.com.cn
+scribd.com
+archive.org
+nicovideo.jp
+cntv.cn
+themeforest.net
+taringa.net
+cam4.com
+yesky.com
+wsj.com
+csdn.net
+isohunt.com
+imageshack.us
+digg.com
+soku.com
+orange.fr
+w3schools.com
+tripadvisor.com
+soundcloud.com
+it168.com
+ameba.jp
+sweetim.com
+tagged.com
+telegraph.co.uk
+web.de
+hostgator.com
+4399.com
+indeed.com
+xing.com
+rambler.ru
+wp.pl
+reuters.com
+nbcolympics.com
+gmx.net
+pof.com
+samsung.com
+funmoods.com
+ku6.com
+ilivid.com
+blogspot.com.es
+angege.com
+libero.it
+ikea.com
+forbes.com
+hardsextube.com
+kaixin001.com
+ups.com
+ero-advertising.com
+china.com
+hatena.ne.jp
+mashable.com
+hp.com
+aizhan.com
+hudong.com
+9gag.com
+media.tumblr.com
+youdao.com
+adserverplus.com
+php.net
+y8.com
+bitauto.com
+clickbank.com
+espncricinfo.com
+soufun.com
+rutracker.org
+mercadolivre.com.br
+flipkart.com
+target.com
+linkwithin.com
+terra.com.br
+amazon.cn
+wordreference.com
+kakaku.com
+paipai.com
+narod.ru
+twimg.com
+thefreedictionary.com
+getfirebug.com
+kaskus.co.id
+drtuber.com
+weebly.com
+zimbio.com
+typepad.com
+xcar.com.cn
+akamaihd.net
+goal.com
+pandora.com
+milliyet.com.tr
+ezinearticles.com
+groupon.com
+cj.com
+iminent.com
+putlocker.com
+xe.com
+agoda.com
+github.com
+rapidgator.net
+mop.com
+hulu.com
+2345.com
+hurriyet.com.tr
+washingtonpost.com
+maktoob.com
+ganji.com
+dell.com
+ebay.com.au
+blogspot.jp
+daum.net
+mixi.jp
+dianxin.cn
+infusionsoft.com
+homeway.com.cn
+hubpages.com
+aliexpress.com
+ig.com.br
+dianping.com
+tribalfusion.com
+match.com
+leo.org
+ebay.it
+blogspot.de
+gazeta.pl
+mlb.com
+usps.com
+constantcontact.com
+t-online.de
+alimama.com
+uimserv.net
+shutterstock.com
+businessinsider.com
+uploaded.to
+bet365.com
+battle.net
+bestbuy.com
+letv.com
+51.la
+usatoday.com
+foxsports.com
+twoo.com
+abcnews.go.com
+americanexpress.com
+pptv.com
+seznam.cz
+att.com
+in.com
+mycalendarbook.com
+kooora.com
+51job.com
+elpais.com
+admin5.com
+digitalpoint.com
+movie2k.to
+autohome.com.cn
+analyrics.com
+mpnrs.com
+repubblica.it
+varzesh3.com
+webs.com
+yourlust.com
+detik.com
+126.com
+seesaa.net
+marca.com
+gutefrage.net
+nifty.com
+extratorrent.com
+freelancer.com
+huanqiu.com
+expedia.com
+joomla.org
+mailchimp.com
+nih.gov
+zanox.com
+pch.com
+chinanews.com
+2ch.net
+adultadworld.com
+letitbit.net
+pchome.net
+ign.com
+popads.net
+elmundo.es
+10086.cn
+blogspot.mx
+amazon.fr
+vnexpress.net
+latimes.com
+nbcnews.com
+histats.com
+w3.org
+naukri.com
+ebay.fr
+istockphoto.com
+wretch.cc
+youm7.com
+techcrunch.com
+bluehost.com
+nuvid.com
+over-blog.com
+orkut.com
+imesh.com
+twitpic.com
+peyvandha.ir
+jobrapido.com
+arpg2.com
+mihanblog.com
+fedex.com
+ning.com
+bloomberg.com
+bleacherreport.com
+speedtest.net
+yandex.ua
+turbobit.net
+wikihow.com
+tmz.com
+gsmarena.com
+drudgereport.com
+pcauto.com.cn
+1channel.ch
+39.net
+twcczhu.com
+siteadvisor.com
+nasa.gov
+linkbucks.com
+capitalone.com
+multiply.com
+sakura.ne.jp
+jiayuan.com
+hypergames.net
+seomoz.org
+dmm.co.jp
+engadget.com
+rbc.ru
+chip.de
+verizonwireless.com
+avito.ru
+hdfcbank.com
+114so.cn
+rr.com
+irctc.co.in
+warriorplus.com
+enet.com.cn
+meetup.com
+ebay.in
+empowernetwork.com
+jimdo.com
+monster.com
+commentcamarche.net
+beeg.com
+biglobe.ne.jp
+orkut.com.br
+lemonde.fr
+odesk.com
+ask.fm
+dangdang.com
+kijiji.ca
+189.cn
+bravotube.net
+vancl.com
+cnblogs.com
+informer.com
+zillow.com
+fotolia.com
+mynet.com
+etao.com
+goodreads.com
+blogspot.it
+777wyx.com
+wix.com
+naver.jp
+sinaimg.cn
+ovh.net
+21cn.com
+surveymonkey.com
+spankwire.com
+tabelog.com
+mapquest.com
+ndtv.com
+linkbucksmedia.com
+sape.ru
+buzzfeed.com
+miniclip.com
+corriere.it
+lenta.ru
+shaadi.com
+website-unavailable.com
+viadeo.com
+pclady.com.cn
+xda-developers.com
+zing.vn
+softpedia.com
+itau.com.br
+bitly.com
+abril.com.br
+thesun.co.uk
+sahibinden.com
+neobux.com
+lzjl.com
+51.com
+weather.com.cn
+icbc.com.cn
+people.com
+lefigaro.fr
+qunar.com
+webmd.com
+delicious.com
+exoclick.com
+livedoor.biz
+java.com
+microsoftonline.com
+yomiuri.co.jp
+immobilienscout24.de
+shopathome.com
+drupal.org
+eastmoney.com
+keezmovies.com
+virgilio.it
+zhaopin.com
+blackhatworld.com
+mobile.de
+blog.163.com
+myfreecams.com
+newegg.com
+115.com
+accuweather.com
+homedepot.com
+zynga.com
+netlog.com
+lequipe.fr
+msn.com.cn
+cnr.cn
+hupu.com
+ibm.com
+gotomeeting.com
+slutload.com
+icicibank.com
+persianblog.ir
+livestrong.com
+mercadolibre.com.ar
+ya.ru
+news.com.au
+123rf.com
+duowan.com
+alphaporno.com
+888.com
+xtube.com
+taleo.net
+wetter.com
+gc.ca
+jxliu.com
+tradedoubler.com
+pornhublive.com
+pogo.com
+wunderground.com
+hotels.com
+retailmenot.com
+mercadolibre.com.mx
+oneindia.in
+backpage.com
+steampowered.com
+feedburner.com
+nikkei.com
+gi-akademie.com
+livingsocial.com
+largeporntube.com
+webmoney.ru
+masrawy.com
+zwaar.net
+clixsense.com
+disney.go.com
+outlook.com
+gmw.cn
+heise.de
+cbsnews.com
+allrecipes.com
+classifiedsgiant.com
+xyxy.net
+swagbucks.com
+7k7k.com
+imagebam.com
+sponichi.co.jp
+examiner.com
+ancestry.com
+www.net.cn
+mybrowserbar.com
+searchqu.com
+nokia.com
+hotfile.com
+yihaodian.com
+nu.nl
+nhk.or.jp
+gi-backoffice.com
+altervista.org
+careerbuilder.com
+r7.com
+pcgames.com.cn
+macys.com
+exblog.jp
+porn.com
+myegy.com
+tinypic.com
+issuu.com
+porntube.com
+adscale.de
+so-net.ne.jp
+kinopoisk.ru
+priceline.com
+excite.co.jp
+oracle.com
+verizon.com
+jquery.com
+snapdeal.com
+quibids.com
+focus.cn
+yellowpages.com
+way2sms.com
+okwave.jp
+ustream.tv
+verycd.com
+china.com.cn
+interia.pl
+fatakat.com
+asahi.com
+justdial.com
+quikr.com
+folha.uol.com.br
+who.is
+nydailynews.com
+trulia.com
+okcupid.com
+imagevenue.com
+nba.com
+tabnak.ir
+force.com
+manta.com
+bitshare.com
+inbox.com
+aili.com
+lady8844.com
+mp3skull.com
+payza.com
+stackexchange.com
+suning.com
+marketwatch.com
+blekko.com
+elance.com
+shareasale.com
+custhelp.com
+sapo.pt
+yoka.com
+olx.in
+sitesell.com
+bodybuilding.com
+haberturk.com
+discuz.net
+blog.com
+timeanddate.com
+habrahabr.ru
+iteye.com
+baixing.com
+filehippo.com
+pixnet.net
+wiktionary.org
+getresponse.com
+ccb.com
+mangareader.net
+ocn.ne.jp
+time.com
+kickstarter.com
+hi5.com
+infolinks.com
+v9.com
+coupons.com
+moneycontrol.com
+quora.com
+cloob.com
+jumbofiles.com
+friv.com
+wired.com
+adjuggler.net
+sulekha.com
+qidian.com
+freakshare.com
+cheshi.com.cn
+bannersbroker.com
+searchengines.ru
+alarabiya.net
+templatemonster.com
+metacafe.com
+cnbc.com
+cocolog-nifty.com
+welt.de
+japanpost.jp
+watchseries.eu
+onlylady.com
+gap.com
+pixiv.net
+list-manage.com
+alot.com
+gamespot.com
+52pk.net
+118114.cn
+amung.us
+as.com
+hc360.com
+tnaflix.com
+skyrock.com
+h2porn.com
+blogimg.jp
+blackberry.com
+cbslocal.com
+babycenter.com
+nikkeibp.co.jp
+facenama.com
+whitepages.com
+sears.com
+seowhy.com
+kayak.com
+kompas.com
+dict.cc
+codecanyon.net
+ctrip.com
+pornerbros.com
+zippyshare.com
+tutsplus.com
+userapi.com
+zendesk.com
+am10.ru
+focus.de
+kdnet.net
+1und1.de
+geocities.jp
+4tube.com
+indianrail.gov.in
+dantri.com.vn
+bigpoint.com
+18schoolgirlz.com
+ca.gov
+linksynergy.com
+aftonbladet.se
+tubegalore.com
+partypoker.com
+klikbca.com
+idnes.cz
+video-one.com
+24h.com.vn
+justin.tv
+usbank.com
+tataindicom.com
+xtendmedia.com
+lifehacker.com
+urbandictionary.com
+namecheap.com
+chinabyte.com
+motherless.com
+southwest.com
+blogspot.com.ar
+cmbchina.com
+gismeteo.ru
+slickdeals.net
+logsoku.com
+adult-empire.com
+makeuseof.com
+businessweek.com
+mbc.net
+howstuffworks.com
+orf.at
+mtime.com
+telegraaf.nl
+pagesjaunes.fr
+roulettebotplus.com
+exposedwebcams.com
+onlinesbi.com
+kohls.com
+rottentomatoes.com
+dh818.com
+demonoid.me
+barnesandnoble.com
+dmoz.org
+sanook.com
+ucoz.com
+woot.com
+hubspot.com
+intuit.com
+td.com
+gumtree.com
+hsbc.co.uk
+nikkansports.com
+smh.com.au
+bhaskar.com
+marktplaats.nl
+bahn.de
+tom.com
+panet.co.il
+overstock.com
+enterfactory.com
+rakuten.ne.jp
+pcworld.com
+nordstrom.com
+traidnt.net
+acesse.com
+akhbarak.net
+citibank.com
+foodnetwork.com
+gamefaqs.com
+fotostrana.ru
+nextag.com
+mercadolibre.com.ve
+zappos.com
+eastday.com
+jabong.com
+anonym.to
+foursquare.com
+rightmove.co.uk
+softlayer.com
+tenpay.com
+rednet.cn
+nudevista.com
+televisionfanatic.com
+sueddeutsche.de
+hidemyass.com
+cbssports.com
+meituan.com
+discoverbing.com
+cnbeta.com
+rtl.de
+nike.com
+alexa.com
+doorblog.jp
+goo.gl
+allocine.fr
+meilishuo.com
+national-lottery.co.uk
+ahram.org.eg
+solidtrustpay.com
+dpreview.com
+babytree.com
+yxlady.com
+120ask.com
+sanspo.com
+citrixonline.com
+realtor.com
+lowes.com
+video-lyrics.com
+18andabused.com
+admagnet.net
+novinky.cz
+musica.com
+asos.com
+menepe.com
+bartarinha.ir
+yandex.kz
+subito.it
+19lou.com
+eventbrite.com
+alnaddy.com
+klout.com
+yiqifa.com
+yfrog.com
+gizmodo.com
+cracked.com
+posterous.com
+tinyurl.com
+subscene.com
+sfr.fr
+xvideoslive.com
+linternaute.com
+bbc.com
+eyny.com
+smashingmagazine.com
+clickbank.net
+6.cn
+aeriagames.com
+legacy.com
+glispa.com
+npr.org
+impress.co.jp
+beemp3.com
+aljazeera.net
+magentocommerce.com
+51cto.com
+bearshare.net
+farsnews.com
+perfectgirls.net
+auto.ru
+empflix.com
+liveleak.com
+vente-privee.com
+allabout.co.jp
+qq937.com
+ziddu.com
+patch.com
+att.net
+last.fm
+ixxx.com
+zoho.com
+thechive.com
+skysports.com
+android.com
+logmein.com
+subscribe.ru
+zeekler.com
+icontact.com
+earthlink.net
+itmedia.co.jp
+udn.com
+mysql.com
+rt.com
+semrush.com
+17173.com
+onlinedown.net
+meteofrance.com
+trafficholder.com
+qip.ru
+filecrop.com
+marketgid.com
+speedbit.com
+sdo.com
+xbox.com
+europa.eu
+caixa.gov.br
+gamer.com.tw
+zhubajie.com
+4chan.org
+chinabroadcast.cn
+ninemsn.com.au
+cloudfront.net
+ebay.ca
+worldstarhiphop.com
+ppstream.com
+lacaixa.es
+mediaset.it
+dyndns.org
+kuxun.cn
+yobt.com
+freeones.com
+jugem.jp
+flippa.com
+mail.com
+delta.com
+boston.com
+slando.ru
+servads.com
+extremetube.com
+filefactory.com
+tuenti.com
+noaa.gov
+commbank.com.au
+correios.com.br
+piriform.com
+theplanet.com
+blogspot.com.au
+dreamstime.com
+united.com
+mydomainadvisor.com
+bidorbuy.co.za
+liveperson.net
+opera.com
+weather.gov
+craigslist.ca
+radikal.ru
+avast.com
+disqus.com
+nationalgeographic.com
+sky.com
+idealo.de
+quickmeme.com
+nipic.com
+sfgate.com
+sergey-mavrodi.com
+gazzetta.it
+sunporno.com
+ultimatepowerprofits.com
+docin.com
+tf1.fr
+skycn.com
+dtiblog.com
+accountonline.com
+meetcheap.com
+macrumors.com
+opensiteexplorer.org
+nuomi.com
+m-w.com
+laredoute.fr
+lenovo.com
+pornoxo.com
+brothersoft.com
+ticketmaster.com
+eluniversal.com.mx
+mbank.com.pl
+costco.com
+fixya.com
+docstoc.com
+cy-pr.com
+biblegateway.com
+sockshare.com
+usmagazine.com
+makemytrip.com
+jalan.net
+youtube-mp3.org
+uwavou.com
+weblio.jp
+jeuxvideo.com
+nfl.com
+independent.co.uk
+msn.ca
+easyhits4u.com
+ria.ru
+staples.com
+inetgiant.com
+behance.net
+ebay.es
+sendspace.com
diff --git a/tools/telemetry/run_tests b/tools/telemetry/run_tests
new file mode 100755
index 0000000..43a9ec2
--- /dev/null
+++ b/tools/telemetry/run_tests
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import sys
+
+import telemetry.run_tests
+
+if __name__ == '__main__':
+ top_level_dir = os.path.abspath(
+ os.path.dirname(__file__))
+ start_dir = 'telemetry'
+ ret = telemetry.run_tests.Main(
+ sys.argv[1:], start_dir, top_level_dir)
+ sys.exit(ret)
diff --git a/tools/telemetry/telemetry/__init__.py b/tools/telemetry/telemetry/__init__.py
new file mode 100644
index 0000000..04f488e
--- /dev/null
+++ b/tools/telemetry/telemetry/__init__.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""A library for chrome-based tests.
+
+"""
+from telemetry.browser import Browser
+from telemetry.browser_finder import FindBrowser
+from telemetry.browser_finder import GetAllAvailableBrowserTypes
+from telemetry.browser_gone_exception import BrowserGoneException
+from telemetry.browser_options import BrowserOptions
+from telemetry.tab import Tab
+from telemetry.tab_crash_exception import TabCrashException
+from telemetry.util import TimeoutException, WaitFor
+
+def CreateBrowser(browser_type):
+ """Shorthand way to create a browser of a given type
+
+ However, note that the preferred way to create a browser is:
+ options = BrowserOptions()
+ _, leftover_args, = options.CreateParser().parse_args()
+ browser_to_create = FindBrowser(options)
+ return browser_to_create.Create()
+
+ as it creates more opportunities for customization and
+ error handling."""
+ browser_to_create = FindBrowser(BrowserOptions(browser_type))
+ if not browser_to_create:
+ raise Exception('No browser of type %s found' % browser_type)
+ return browser_to_create.Create()
diff --git a/tools/telemetry/telemetry/adb_commands.py b/tools/telemetry/telemetry/adb_commands.py
new file mode 100644
index 0000000..a1dbb40
--- /dev/null
+++ b/tools/telemetry/telemetry/adb_commands.py
@@ -0,0 +1,152 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Brings in Chrome Android's android_commands module, which itself is a
+thin(ish) wrapper around adb."""
+import os
+import sys
+
+# This is currently a thin wrapper around Chrome Android's
+# build scripts, located in chrome/build/android. This file exists mainly to
+# deal with locating the module.
+
+# Get build/android scripts into our path.
+sys.path.append(
+ os.path.abspath(
+ os.path.join(os.path.dirname(__file__),
+ '../../../build/android')))
+try:
+ from pylib import android_commands # pylint: disable=F0401
+ from pylib import forwarder # pylint: disable=F0401
+ from pylib import valgrind_tools # pylint: disable=F0401
+except Exception:
+ android_commands = None
+
+def IsAndroidSupported():
+ return android_commands != None
+
+def GetAttachedDevices():
+ """Returns a list of attached, online android devices.
+
+ If a preferred device has been set with ANDROID_SERIAL, it will be first in
+ the returned list."""
+ return android_commands.GetAttachedDevices()
+
+class AdbCommands(object):
+ """A thin wrapper around ADB"""
+
+ def __init__(self, device):
+ self._adb = android_commands.AndroidCommands(device)
+
+ def Adb(self):
+ return self._adb
+
+ def Forward(self, local, remote):
+ ret = self._adb.Adb().SendCommand('forward %s %s' % (local, remote))
+ assert ret == ''
+
+ def RunShellCommand(self, command, timeout_time=20, log_result=False):
+ """Send a command to the adb shell and return the result.
+
+ Args:
+ command: String containing the shell command to send. Must not include
+ the single quotes as we use them to escape the whole command.
+ timeout_time: Number of seconds to wait for command to respond before
+ retrying, used by AdbInterface.SendShellCommand.
+ log_result: Boolean to indicate whether we should log the result of the
+ shell command.
+
+ Returns:
+ list containing the lines of output received from running the command
+ """
+ return self._adb.RunShellCommand(command, timeout_time, log_result)
+
+ def KillAll(self, process):
+ """Android version of killall, connected via adb.
+
+ Args:
+ process: name of the process to kill off
+
+ Returns:
+ the number of processess killed
+ """
+ return self._adb.KillAll(process)
+
+ def ExtractPid(self, process_name):
+ """Extracts Process Ids for a given process name from Android Shell.
+
+ Args:
+ process_name: name of the process on the device.
+
+ Returns:
+ List of all the process ids (as strings) that match the given name.
+ If the name of a process exactly matches the given name, the pid of
+ that process will be inserted to the front of the pid list.
+ """
+ return self._adb.ExtractPid(process_name)
+
+ def StartActivity(self, package, activity, wait_for_completion=False,
+ action='android.intent.action.VIEW',
+ category=None, data=None,
+ extras=None, trace_file_name=None):
+ """Starts |package|'s activity on the device.
+
+ Args:
+ package: Name of package to start (e.g. 'com.google.android.apps.chrome').
+ activity: Name of activity (e.g. '.Main' or
+ 'com.google.android.apps.chrome.Main').
+ wait_for_completion: wait for the activity to finish launching (-W flag).
+ action: string (e.g. 'android.intent.action.MAIN'). Default is VIEW.
+ category: string (e.g. 'android.intent.category.HOME')
+ data: Data string to pass to activity (e.g. 'http://www.example.com/').
+ extras: Dict of extras to pass to activity. Values are significant.
+ trace_file_name: If used, turns on and saves the trace to this file name.
+ """
+ return self._adb.StartActivity(package, activity, wait_for_completion,
+ action,
+ category, data,
+ extras, trace_file_name)
+
+ def Push(self, local, remote):
+ return self._adb.Adb().Push(local, remote)
+
+ def Pull(self, remote, local):
+ return self._adb.Adb().Pull(remote, local)
+
+ def FileExistsOnDevice(self, file_name):
+ return self._adb.FileExistsOnDevice(file_name)
+
+ def IsRootEnabled(self):
+ return self._adb.IsRootEnabled()
+
+def HasForwarder(adb, buildtype=None):
+ if not buildtype:
+ return (HasForwarder(adb, buildtype='Release') or
+ HasForwarder(adb, buildtype='Debug'))
+ return (os.path.exists(os.path.join('out', buildtype, 'device_forwarder')) and
+ os.path.exists(os.path.join('out', buildtype, 'host_forwarder')))
+
+class Forwarder(object):
+ def __init__(self, adb, *ports):
+ assert HasForwarder(adb)
+
+ port_pairs = [(port, port) for port in ports]
+ tool = valgrind_tools.BaseTool()
+
+ self._host_port = ports[0]
+ buildtype = 'Debug'
+ if HasForwarder(adb, 'Release'):
+ buildtype = 'Release'
+ self._forwarder = forwarder.Forwarder(
+ adb.Adb(), port_pairs,
+ tool, '127.0.0.1', buildtype)
+
+ @property
+ def url(self):
+ assert self._forwarder
+ return 'http://localhost:%i' % self._host_port
+
+ def Close(self):
+ if self._forwarder:
+ self._forwarder.Close()
+ self._forwarder = None
diff --git a/tools/telemetry/telemetry/android_browser_backend.py b/tools/telemetry/telemetry/android_browser_backend.py
new file mode 100644
index 0000000..b5aaa62
--- /dev/null
+++ b/tools/telemetry/telemetry/android_browser_backend.py
@@ -0,0 +1,125 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import logging
+import tempfile
+import json
+
+from telemetry import adb_commands
+from telemetry import browser_backend
+from telemetry import browser_gone_exception
+
+class AndroidBrowserBackend(browser_backend.BrowserBackend):
+ """The backend for controlling a browser instance running on Android.
+ """
+ def __init__(self, options, adb, package,
+ is_content_shell, cmdline_file, activity, devtools_remote_port):
+ super(AndroidBrowserBackend, self).__init__(is_content_shell, options)
+ # Initialize fields so that an explosion during init doesn't break in Close.
+ self._options = options
+ self._adb = adb
+ self._package = package
+ self._cmdline_file = cmdline_file
+ self._activity = activity
+ self._port = 9222
+ self._devtools_remote_port = devtools_remote_port
+
+ # Kill old browser.
+ self._adb.KillAll(self._package)
+ self._adb.KillAll('device_forwarder')
+ self._adb.Forward('tcp:9222', self._devtools_remote_port)
+
+ # Chrome Android doesn't listen to --user-data-dir.
+ # TODO: symlink the app's Default, files and cache dir
+ # to somewhere safe.
+ if not is_content_shell and not options.dont_override_profile:
+ # Set up the temp dir
+ # self._tmpdir = '/sdcard/telemetry_data'
+ # self._adb.RunShellCommand('rm -r %s' % self._tmpdir)
+ # args.append('--user-data-dir=%s' % self._tmpdir)
+ pass
+
+ # Set up the command line.
+ if is_content_shell:
+ pseudo_exec_name = 'content_shell'
+ else:
+ pseudo_exec_name = 'chrome'
+
+ args = [pseudo_exec_name]
+ args.extend(self.GetBrowserStartupArgs())
+
+ with tempfile.NamedTemporaryFile() as f:
+ def EscapeIfNeeded(arg):
+ return arg.replace(' ', '" "')
+ f.write(' '.join([EscapeIfNeeded(arg) for arg in args]))
+ f.flush()
+ self._adb.Push(f.name, cmdline_file)
+
+ # Force devtools protocol on, if not already done.
+ if not is_content_shell:
+ # Make sure we can find the apps' prefs file
+ app_data_dir = '/data/data/%s' % self._package
+ prefs_file = (app_data_dir +
+ '/app_chrome/Default/Preferences')
+ if not self._adb.FileExistsOnDevice(prefs_file):
+ logging.critical(
+ 'android_browser_backend: Could not find preferences file ' +
+ '%s for %s' % (prefs_file, self._package))
+ raise browser_gone_exception.BrowserGoneException(
+ 'Missing preferences file.')
+
+ with tempfile.NamedTemporaryFile() as raw_f:
+ self._adb.Pull(prefs_file, raw_f.name)
+ with open(raw_f.name, 'r') as f:
+ txt_in = f.read()
+ preferences = json.loads(txt_in)
+ changed = False
+ if 'devtools' not in preferences:
+ preferences['devtools'] = {}
+ changed = True
+ if 'remote_enabled' not in preferences['devtools']:
+ preferences['devtools']['remote_enabled'] = True
+ changed = True
+ if preferences['devtools']['remote_enabled'] != True:
+ preferences['devtools']['remote_enabled'] = True
+ changed = True
+ if changed:
+ logging.warning('Manually enabled devtools protocol on %s' %
+ self._package)
+ with open(raw_f.name, 'w') as f:
+ txt = json.dumps(preferences, indent=2)
+ f.write(txt)
+ self._adb.Push(raw_f.name, prefs_file)
+
+ # Start it up!
+ self._adb.StartActivity(self._package,
+ self._activity,
+ True,
+ None,
+ 'chrome://newtab/')
+ try:
+ self._WaitForBrowserToComeUp()
+ except:
+ import traceback
+ traceback.print_exc()
+ self.Close()
+ raise
+
+ def GetBrowserStartupArgs(self):
+ args = super(AndroidBrowserBackend, self).GetBrowserStartupArgs()
+ args.append('--disable-fre')
+ return args
+
+ def __del__(self):
+ self.Close()
+
+ def Close(self):
+ self._adb.RunShellCommand('rm %s' % self._cmdline_file)
+ self._adb.KillAll(self._package)
+
+ def IsBrowserRunning(self):
+ pids = self._adb.ExtractPid(self._package)
+ return len(pids) != 0
+
+ def CreateForwarder(self, *ports):
+ return adb_commands.Forwarder(self._adb, *ports)
diff --git a/tools/telemetry/telemetry/android_browser_finder.py b/tools/telemetry/telemetry/android_browser_finder.py
new file mode 100644
index 0000000..83e1030
--- /dev/null
+++ b/tools/telemetry/telemetry/android_browser_finder.py
@@ -0,0 +1,149 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Finds android browsers that can be controlled by telemetry."""
+
+import os
+import logging as real_logging
+import re
+import subprocess
+
+from telemetry import adb_commands
+from telemetry import android_browser_backend
+from telemetry import android_platform
+from telemetry import browser
+from telemetry import possible_browser
+
+ALL_BROWSER_TYPES = ','.join([
+ 'android-content-shell',
+ 'android-chrome',
+ 'android-jb-system-chrome',
+ ])
+
+CHROME_PACKAGE = 'com.google.android.apps.chrome'
+CHROME_ACTIVITY = '.Main'
+CHROME_COMMAND_LINE = '/data/local/chrome-command-line'
+CHROME_DEVTOOLS_REMOTE_PORT = 'localabstract:chrome_devtools_remote'
+
+CHROME_JB_SYSTEM_PACKAGE = 'com.android.chrome'
+CHROME_JB_SYSTEM_DEVTOOLS_REMOTE_PORT = 'localabstract:chrome_devtools_remote'
+
+CONTENT_SHELL_PACKAGE = 'org.chromium.content_shell'
+CONTENT_SHELL_ACTIVITY = '.ContentShellActivity'
+CONTENT_SHELL_COMMAND_LINE = '/data/local/tmp/content-shell-command-line'
+CONTENT_SHELL_DEVTOOLS_REMOTE_PORT = (
+ 'localabstract:content_shell_devtools_remote')
+
+# adb shell pm list packages
+# adb
+# intents to run (pass -D url for the rest)
+# com.android.chrome/.Main
+# com.google.android.apps.chrome/.Main
+
+class PossibleAndroidBrowser(possible_browser.PossibleBrowser):
+ """A launchable android browser instance."""
+ def __init__(self, browser_type, options, *args):
+ super(PossibleAndroidBrowser, self).__init__(
+ browser_type, options)
+ self._args = args
+
+ def __repr__(self):
+ return 'PossibleAndroidBrowser(browser_type=%s)' % self.browser_type
+
+ def Create(self):
+ backend = android_browser_backend.AndroidBrowserBackend(
+ self._options, *self._args)
+ platform = android_platform.AndroidPlatform(
+ self._args[0].Adb(), self._args[1],
+ self._args[1] + self._args[4])
+ return browser.Browser(backend, platform)
+
+def FindAllAvailableBrowsers(options, logging=real_logging):
+ """Finds all the desktop browsers available on this machine."""
+ if not adb_commands.IsAndroidSupported():
+ return []
+
+ # See if adb even works.
+ try:
+ with open(os.devnull, 'w') as devnull:
+ proc = subprocess.Popen(['adb', 'devices'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=devnull)
+ stdout, _ = proc.communicate()
+ if re.search(re.escape('????????????\tno permissions'), stdout) != None:
+ logging.warn(
+ ('adb devices reported a permissions error. Consider '
+ 'restarting adb as root:'))
+ logging.warn(' adb kill-server')
+ logging.warn(' sudo `which adb` devices\n\n')
+ except OSError:
+ logging.info('No adb command found. ' +
+ 'Will not try searching for Android browsers.')
+ return []
+
+ device = None
+ if not options.android_device:
+ devices = adb_commands.GetAttachedDevices()
+ else:
+ devices = []
+
+ if len(devices) == 0:
+ logging.info('No android devices found.')
+ return []
+
+ if len(devices) > 1:
+ logging.warn('Multiple devices attached. ' +
+ 'Please specify a device explicitly.')
+ return []
+
+ device = devices[0]
+
+ adb = adb_commands.AdbCommands(device=device)
+
+ # See if adb is root
+ if not adb.IsRootEnabled():
+ logging.warn('ADB is not root. Please make it root by doing:')
+ logging.warn(' adb root')
+ return []
+
+ packages = adb.RunShellCommand('pm list packages')
+ possible_browsers = []
+ if 'package:' + CONTENT_SHELL_PACKAGE in packages:
+ b = PossibleAndroidBrowser('android-content-shell',
+ options, adb,
+ CONTENT_SHELL_PACKAGE, True,
+ CONTENT_SHELL_COMMAND_LINE,
+ CONTENT_SHELL_ACTIVITY,
+ CONTENT_SHELL_DEVTOOLS_REMOTE_PORT)
+ possible_browsers.append(b)
+
+ if 'package:' + CHROME_PACKAGE in packages:
+ b = PossibleAndroidBrowser('android-chrome',
+ options, adb,
+ CHROME_PACKAGE, False,
+ CHROME_COMMAND_LINE,
+ CHROME_ACTIVITY,
+ CHROME_DEVTOOLS_REMOTE_PORT)
+ possible_browsers.append(b)
+
+ if 'package:' + CHROME_JB_SYSTEM_PACKAGE in packages:
+ b = PossibleAndroidBrowser('android-jb-system-chrome',
+ options, adb,
+ CHROME_JB_SYSTEM_PACKAGE, False,
+ CHROME_COMMAND_LINE,
+ CHROME_ACTIVITY,
+ CHROME_JB_SYSTEM_DEVTOOLS_REMOTE_PORT)
+ possible_browsers.append(b)
+
+ # See if the "forwarder" is installed -- we need this to host content locally
+ # but make it accessible to the device.
+ if len(possible_browsers) and not adb_commands.HasForwarder(adb):
+ logging.warn('telemetry detected an android device. However,')
+ logging.warn('Chrome\'s port-forwarder app is not available.')
+ logging.warn('To build:')
+ logging.warn(' make -j16 host_forwarder device_forwarder')
+ logging.warn('')
+ logging.warn('')
+ return []
+ return possible_browsers
diff --git a/tools/telemetry/telemetry/android_browser_finder_unittest.py b/tools/telemetry/telemetry/android_browser_finder_unittest.py
new file mode 100644
index 0000000..ef1c75b
--- /dev/null
+++ b/tools/telemetry/telemetry/android_browser_finder_unittest.py
@@ -0,0 +1,88 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+
+from telemetry import android_browser_finder
+from telemetry import browser_options
+from telemetry import system_stub
+
+class LoggingStub(object):
+ def __init__(self):
+ self.warnings = []
+
+ def info(self, msg, *args):
+ pass
+
+ def warn(self, msg, *args):
+ self.warnings.append(msg % args)
+
+class AndroidBrowserFinderTest(unittest.TestCase):
+ def setUp(self):
+ self._stubs = system_stub.Override(android_browser_finder,
+ ['adb_commands', 'subprocess'])
+
+ def tearDown(self):
+ self._stubs.Restore()
+
+ def test_no_adb(self):
+ options = browser_options.BrowserOptions()
+
+ def NoAdb(*args, **kargs): # pylint: disable=W0613
+ raise OSError('not found')
+ self._stubs.subprocess.Popen = NoAdb
+ browsers = android_browser_finder.FindAllAvailableBrowsers(options)
+ self.assertEquals(0, len(browsers))
+
+ def test_adb_no_devices(self):
+ options = browser_options.BrowserOptions()
+
+ browsers = android_browser_finder.FindAllAvailableBrowsers(options)
+ self.assertEquals(0, len(browsers))
+
+
+ def test_adb_permissions_error(self):
+ options = browser_options.BrowserOptions()
+
+ self._stubs.subprocess.Popen.communicate_result = (
+ """List of devices attached
+????????????\tno permissions""",
+ """* daemon not running. starting it now on port 5037 *
+* daemon started successfully *
+""")
+
+ log_stub = LoggingStub()
+ browsers = android_browser_finder.FindAllAvailableBrowsers(
+ options, log_stub)
+ self.assertEquals(3, len(log_stub.warnings))
+ self.assertEquals(0, len(browsers))
+
+
+ def test_adb_two_devices(self):
+ options = browser_options.BrowserOptions()
+
+ self._stubs.adb_commands.attached_devices = ['015d14fec128220c',
+ '015d14fec128220d']
+
+ log_stub = LoggingStub()
+ browsers = android_browser_finder.FindAllAvailableBrowsers(
+ options, log_stub)
+ self.assertEquals(1, len(log_stub.warnings))
+ self.assertEquals(0, len(browsers))
+
+ def test_adb_one_device(self):
+ options = browser_options.BrowserOptions()
+
+ self._stubs.adb_commands.attached_devices = ['015d14fec128220c']
+
+ def OnPM(args):
+ assert args[0] == 'pm'
+ assert args[1] == 'list'
+ assert args[2] == 'packages'
+ return ['package:org.chromium.content_shell',
+ 'package.com.google.android.setupwizard']
+
+ self._stubs.adb_commands.shell_command_handlers['pm'] = OnPM
+
+ browsers = android_browser_finder.FindAllAvailableBrowsers(options)
+ self.assertEquals(1, len(browsers))
diff --git a/tools/telemetry/telemetry/android_platform.py b/tools/telemetry/telemetry/android_platform.py
new file mode 100644
index 0000000..6e544d7
--- /dev/null
+++ b/tools/telemetry/telemetry/android_platform.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import sys
+
+# Get build/android scripts into our path.
+sys.path.append(
+ os.path.abspath(
+ os.path.join(os.path.dirname(__file__),
+ '../../../build/android')))
+try:
+ from pylib import surface_stats_collector # pylint: disable=F0401
+except Exception:
+ surface_stats_collector = None
+
+
+class AndroidPlatform(object):
+ def __init__(self, adb, window_package, window_activity):
+ super(AndroidPlatform, self).__init__()
+ self._adb = adb
+ self._window_package = window_package
+ self._window_activity = window_activity
+
+ def GetSurfaceCollector(self, trace_tag):
+ return surface_stats_collector.SurfaceStatsCollector(
+ self._adb, self._window_package, self._window_activity, trace_tag)
diff --git a/tools/telemetry/telemetry/browser.py b/tools/telemetry/telemetry/browser.py
new file mode 100644
index 0000000..96dcc9b
--- /dev/null
+++ b/tools/telemetry/telemetry/browser.py
@@ -0,0 +1,110 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+
+from telemetry import temporary_http_server
+from telemetry import browser_credentials
+from telemetry import wpr_modes
+from telemetry import wpr_server
+
+class Browser(object):
+ """A running browser instance that can be controlled in a limited way.
+
+ To create a browser instance, use browser_finder.FindBrowser.
+
+ Be sure to clean up after yourself by calling Close() when you are done with
+ the browser. Or better yet:
+ browser_to_create = FindBrowser(options)
+ with browser_to_create.Create() as browser:
+ ... do all your operations on browser here
+ """
+ def __init__(self, backend, platform):
+ self._backend = backend
+ self._http_server = None
+ self._wpr_server = None
+ self._platform = platform
+ self.credentials = browser_credentials.BrowserCredentials()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.Close()
+
+ @property
+ def is_content_shell(self):
+ """Returns whether this browser is a content shell, only."""
+ return self._backend.is_content_shell
+
+ @property
+ def num_tabs(self):
+ return self._backend.num_tabs
+
+ @property
+ def platform(self):
+ return self._platform
+
+ def NewTab(self):
+ return self._backend.NewTab()
+
+ def CloseTab(self, index):
+ self._backend.CloseTab(index)
+
+ def GetNthTabUrl(self, index):
+ return self._backend.GetNthTabUrl(index)
+
+ def ConnectToNthTab(self, index):
+ return self._backend.ConnectToNthTab(self, index)
+
+ def Close(self):
+ if self._wpr_server:
+ self._wpr_server.Close()
+ self._wpr_server = None
+
+ if self._http_server:
+ self._http_server.Close()
+ self._http_server = None
+
+ self._backend.Close()
+ self.credentials = None
+
+ @property
+ def http_server(self):
+ return self._http_server
+
+ def SetHTTPServerDirectory(self, path):
+ if path:
+ abs_path = os.path.abspath(path)
+ if self._http_server and self._http_server.path == path:
+ return
+ else:
+ abs_path = None
+
+ if self._http_server:
+ self._http_server.Close()
+ self._http_server = None
+
+ if not abs_path:
+ return
+
+ self._http_server = temporary_http_server.TemporaryHTTPServer(
+ self._backend, abs_path)
+
+ def SetReplayArchivePath(self, archive_path):
+ if self._wpr_server:
+ self._wpr_server.Close()
+ self._wpr_server = None
+
+ if not archive_path:
+ return None
+
+ if self._backend.wpr_mode == wpr_modes.WPR_OFF:
+ return
+
+ use_record_mode = self._backend.wpr_mode == wpr_modes.WPR_RECORD
+ if not use_record_mode:
+ assert os.path.isfile(archive_path)
+
+ self._wpr_server = wpr_server.ReplayServer(
+ self._backend, archive_path, use_record_mode)
diff --git a/tools/telemetry/telemetry/browser_backend.py b/tools/telemetry/telemetry/browser_backend.py
new file mode 100644
index 0000000..52fbdb2
--- /dev/null
+++ b/tools/telemetry/telemetry/browser_backend.py
@@ -0,0 +1,115 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import urllib2
+import httplib
+import socket
+import json
+
+from telemetry import browser_gone_exception
+from telemetry import inspector_backend
+from telemetry import tab
+from telemetry import util
+from telemetry import wpr_modes
+from telemetry import wpr_server
+
+class BrowserBackend(object):
+ """A base class for broser backends. Provides basic functionality
+ once a remote-debugger port has been established."""
+ def __init__(self, is_content_shell, options):
+ self.is_content_shell = is_content_shell
+ self.options = options
+ self._port = None
+
+ def GetBrowserStartupArgs(self):
+ args = []
+ args.extend(self.options.extra_browser_args)
+ args.append('--disable-background-networking')
+ args.append('--metrics-recording-only')
+ args.append('--no-first-run')
+ if self.options.wpr_mode != wpr_modes.WPR_OFF:
+ args.extend(wpr_server.CHROME_FLAGS)
+ return args
+
+ @property
+ def wpr_mode(self):
+ return self.options.wpr_mode
+
+ def _WaitForBrowserToComeUp(self):
+ def IsBrowserUp():
+ try:
+ self._ListTabs()
+ except socket.error:
+ if not self.IsBrowserRunning():
+ raise browser_gone_exception.BrowserGoneException()
+ return False
+ except httplib.BadStatusLine:
+ if not self.IsBrowserRunning():
+ raise browser_gone_exception.BrowserGoneException()
+ return False
+ except urllib2.URLError:
+ if not self.IsBrowserRunning():
+ raise browser_gone_exception.BrowserGoneException()
+ return False
+ else:
+ return True
+ try:
+ util.WaitFor(IsBrowserUp, timeout=30)
+ except util.TimeoutException:
+ raise browser_gone_exception.BrowserGoneException()
+
+ @property
+ def _debugger_url(self):
+ return 'http://localhost:%i/json' % self._port
+
+ def _ListTabs(self, timeout=None):
+ req = urllib2.urlopen(self._debugger_url, timeout=timeout)
+ data = req.read()
+ all_contexts = json.loads(data)
+ tabs = [ctx for ctx in all_contexts
+ if not ctx['url'].startswith('chrome-extension://')]
+ # FIXME(dtu): The remote debugger protocol returns in order of most
+ # recently created tab first. In order to convert it to the UI tab
+ # order, we just reverse the list, which assumes we can't move tabs.
+ # We should guarantee that the remote debugger returns in the UI tab order.
+ tabs.reverse()
+ return tabs
+
+ def NewTab(self, timeout=None):
+ req = urllib2.urlopen(self._debugger_url + '/new', timeout=timeout)
+ data = req.read()
+ new_tab = json.loads(data)
+ return new_tab
+
+ def CloseTab(self, index, timeout=None):
+ assert self.num_tabs > 1, 'Closing the last tab not supported.'
+ target_tab = self._ListTabs()[index]
+ tab_id = target_tab['webSocketDebuggerUrl'].split('/')[-1]
+ target_num_tabs = self.num_tabs - 1
+
+ urllib2.urlopen('%s/close/%s' % (self._debugger_url, tab_id),
+ timeout=timeout)
+
+ util.WaitFor(lambda: self.num_tabs == target_num_tabs, timeout=5)
+
+ @property
+ def num_tabs(self):
+ return len(self._ListTabs())
+
+ def GetNthTabUrl(self, index):
+ return self._ListTabs()[index]['url']
+
+ def ConnectToNthTab(self, browser, index):
+ ib = inspector_backend.InspectorBackend(self, self._ListTabs()[index])
+ return tab.Tab(browser, ib)
+
+ def DoesDebuggerUrlExist(self, url):
+ matches = [t for t in self._ListTabs()
+ if t['webSocketDebuggerUrl'] == url]
+ return len(matches) >= 1
+
+ def CreateForwarder(self, host_port):
+ raise NotImplementedError()
+
+ def IsBrowserRunning(self):
+ raise NotImplementedError()
diff --git a/tools/telemetry/telemetry/browser_credentials.py b/tools/telemetry/telemetry/browser_credentials.py
new file mode 100644
index 0000000..9908d91
--- /dev/null
+++ b/tools/telemetry/telemetry/browser_credentials.py
@@ -0,0 +1,131 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import logging
+import json
+import os
+
+from telemetry import facebook_credentials_backend
+from telemetry import google_credentials_backend
+from telemetry import options_for_unittests
+
+class BrowserCredentials(object):
+ def __init__(self, backends = None):
+ self._credentials = {}
+ self._credentials_path = None
+ self._extra_credentials = {}
+
+ if backends is None:
+ backends = [
+ facebook_credentials_backend.FacebookCredentialsBackend(),
+ google_credentials_backend.GoogleCredentialsBackend()]
+
+ self._backends = {}
+ for backend in backends:
+ self._backends[backend.credentials_type] = backend
+
+ def AddBackend(self, backend):
+ assert backend.credentials_type not in self._backends
+ self._backends[backend.credentials_type] = backend
+
+ def CanLogin(self, credentials_type):
+ if credentials_type not in self._backends:
+ raise Exception('Unrecognized credentials type: %s', credentials_type)
+ return credentials_type in self._credentials
+
+ def LoginNeeded(self, tab, credentials_type):
+ if credentials_type not in self._backends:
+ raise Exception('Unrecognized credentials type: %s', credentials_type)
+ if credentials_type not in self._credentials:
+ return False
+ return self._backends[credentials_type].LoginNeeded(
+ tab, self._credentials[credentials_type])
+
+ def LoginNoLongerNeeded(self, tab, credentials_type):
+ assert credentials_type in self._backends
+ self._backends[credentials_type].LoginNoLongerNeeded(tab)
+
+ @property
+ def credentials_path(self):
+ return self._credentials_path
+
+ @credentials_path.setter
+ def credentials_path(self, credentials_path):
+ self._credentials_path = credentials_path
+ self._RebuildCredentials()
+
+ def Add(self, credentials_type, data):
+ if credentials_type not in self._extra_credentials:
+ self._extra_credentials[credentials_type] = {}
+ for k, v in data.items():
+ assert k not in self._extra_credentials[credentials_type]
+ self._extra_credentials[credentials_type][k] = v
+ self._RebuildCredentials()
+
+ def _RebuildCredentials(self):
+ credentials = {}
+ if self._credentials_path == None:
+ pass
+ elif os.path.exists(self._credentials_path):
+ with open(self._credentials_path, 'r') as f:
+ credentials = json.loads(f.read())
+
+ # TODO(nduca): use system keychain, if possible.
+ homedir_credentials_path = os.path.expanduser('~/.telemetry-credentials')
+ homedir_credentials = {}
+
+ if (not options_for_unittests.Get() and
+ os.path.exists(homedir_credentials_path)):
+ logging.info("Found ~/.telemetry-credentials. Its contents will be used "
+ "when no other credentials can be found.")
+ with open(homedir_credentials_path, 'r') as f:
+ homedir_credentials = json.loads(f.read())
+
+ self._credentials = {}
+ all_keys = set(credentials.keys()).union(
+ homedir_credentials.keys()).union(
+ self._extra_credentials.keys())
+
+ for k in all_keys:
+ if k in credentials:
+ self._credentials[k] = credentials[k]
+ if k in homedir_credentials:
+ logging.info("Will use ~/.telemetry-credentials for %s logins." % k)
+ self._credentials[k] = homedir_credentials[k]
+ if k in self._extra_credentials:
+ self._credentials[k] = self._extra_credentials[k]
+
+ def WarnIfMissingCredentials(self, page_set):
+ num_pages_missing_login = 0
+ missing_credentials = set()
+ for page in page_set:
+ if (page.credentials
+ and not self.CanLogin(page.credentials)):
+ num_pages_missing_login += 1
+ missing_credentials.add(page.credentials)
+
+ if num_pages_missing_login > 0:
+ files_to_tweak = []
+ if page_set.credentials_path:
+ files_to_tweak.append(
+ os.path.relpath(os.path.join(page_set.base_dir,
+ page_set.credentials_path)))
+ files_to_tweak.append('~/.telemetry-credentials')
+
+ example_credentials_file = (
+ os.path.relpath(
+ os.path.join(
+ os.path.dirname(__file__),
+ '..', 'examples', 'credentials_example.json')))
+
+ logging.warning("""
+ Credentials for %s were not found. %i pages will not be benchmarked.
+
+ To fix this, either add svn-internal to your .gclient using
+ http://goto/read-src-internal, or add your own credentials to:
+ %s
+ An example credentials file you can copy from is here:
+ %s\n""" % (', '.join(missing_credentials),
+ num_pages_missing_login,
+ ' or '.join(files_to_tweak),
+ example_credentials_file))
diff --git a/tools/telemetry/telemetry/browser_credentials_unittest.py b/tools/telemetry/telemetry/browser_credentials_unittest.py
new file mode 100644
index 0000000..28b0d77
--- /dev/null
+++ b/tools/telemetry/telemetry/browser_credentials_unittest.py
@@ -0,0 +1,68 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+import tempfile
+
+from telemetry import browser_credentials
+
+SIMPLE_CREDENTIALS_STRING = """
+{
+ "google": {
+ "username": "example",
+ "password": "asdf"
+ }
+}
+"""
+
+class BackendStub(object):
+ def __init__(self, credentials_type):
+ self.login_needed_called = None
+ self.login_no_longer_needed_called = None
+ self.credentials_type = credentials_type
+
+ def LoginNeeded(self, config, tab):
+ self.login_needed_called = (config, tab)
+ return True
+
+ def LoginNoLongerNeeded(self, tab):
+ self.login_no_longer_needed_called = (tab, )
+
+
+class TestBrowserCredentials(unittest.TestCase):
+ def testCredentialsInfrastructure(self):
+ google_backend = BackendStub("google")
+ othersite_backend = BackendStub("othersite")
+ browser_cred = browser_credentials.BrowserCredentials(
+ [google_backend,
+ othersite_backend])
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(SIMPLE_CREDENTIALS_STRING)
+ f.flush()
+
+ browser_cred.credentials_path = f.name
+
+ # Should true because it has a password and a backend.
+ self.assertTrue(browser_cred.CanLogin('google'))
+
+ # Should be false succeed because it has no password.
+ self.assertFalse(browser_cred.CanLogin('othersite'))
+
+ # Should fail because it has no backend.
+ self.assertRaises(
+ Exception,
+ lambda: browser_cred.CanLogin('foobar'))
+
+ tab = {}
+ ret = browser_cred.LoginNeeded(tab, 'google')
+ self.assertTrue(ret)
+ self.assertTrue(google_backend.login_needed_called is not None)
+ self.assertEqual(tab, google_backend.login_needed_called[0])
+ self.assertEqual("example",
+ google_backend.login_needed_called[1]["username"])
+ self.assertEqual("asdf",
+ google_backend.login_needed_called[1]["password"])
+
+ browser_cred.LoginNoLongerNeeded(tab, 'google')
+ self.assertTrue(google_backend.login_no_longer_needed_called is not None)
+ self.assertEqual(tab, google_backend.login_no_longer_needed_called[0])
diff --git a/tools/telemetry/telemetry/browser_finder.py b/tools/telemetry/telemetry/browser_finder.py
new file mode 100644
index 0000000..82a98b2
--- /dev/null
+++ b/tools/telemetry/telemetry/browser_finder.py
@@ -0,0 +1,73 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Finds browsers that can be controlled by telemetry."""
+
+import logging
+
+from telemetry import android_browser_finder
+from telemetry import cros_browser_finder
+from telemetry import desktop_browser_finder
+
+ALL_BROWSER_TYPES = (
+ desktop_browser_finder.ALL_BROWSER_TYPES + ',' +
+ android_browser_finder.ALL_BROWSER_TYPES + ',' +
+ cros_browser_finder.ALL_BROWSER_TYPES)
+
+class BrowserTypeRequiredException(Exception):
+ pass
+
+def FindBrowser(options):
+ """Finds the best PossibleBrowser object to run given the provided
+ BrowserOptions object. The returned possiblity object can then be used to
+ connect to and control the located browser.
+ """
+ if options.browser_type == 'exact' and options.browser_executable == None:
+ raise Exception('--browser=exact requires --browser-executable to be set.')
+
+ if options.browser_type != 'exact' and options.browser_executable != None:
+ raise Exception('--browser-executable requires --browser=exact.')
+
+ if options.browser_type == None:
+ raise BrowserTypeRequiredException('browser_type must be specified')
+
+ browsers = []
+ browsers.extend(desktop_browser_finder.FindAllAvailableBrowsers(options))
+ browsers.extend(android_browser_finder.FindAllAvailableBrowsers(options))
+ browsers.extend(cros_browser_finder.FindAllAvailableBrowsers(options))
+
+ if options.browser_type == 'any':
+ types = ALL_BROWSER_TYPES.split(',')
+ def compare_browsers_on_type_priority(x, y):
+ x_idx = types.index(x.browser_type)
+ y_idx = types.index(y.browser_type)
+ return x_idx - y_idx
+ browsers.sort(compare_browsers_on_type_priority)
+ if len(browsers) >= 1:
+ return browsers[0]
+ else:
+ return None
+
+ matching_browsers = [b for b in browsers
+ if b.browser_type == options.browser_type]
+
+ if len(matching_browsers) == 1:
+ return matching_browsers[0]
+ elif len(matching_browsers) > 1:
+ logging.warning('Multiple browsers of the same type found: %s' % (
+ repr(matching_browsers)))
+ return matching_browsers[0]
+ else:
+ return None
+
+def GetAllAvailableBrowserTypes(options):
+ """Returns an array of browser types supported on this system."""
+ browsers = []
+ browsers.extend(desktop_browser_finder.FindAllAvailableBrowsers(options))
+ browsers.extend(android_browser_finder.FindAllAvailableBrowsers(options))
+ browsers.extend(cros_browser_finder.FindAllAvailableBrowsers(options))
+
+ type_list = set([browser.browser_type for browser in browsers])
+ type_list = list(type_list)
+ type_list.sort()
+ return type_list
diff --git a/tools/telemetry/telemetry/browser_gone_exception.py b/tools/telemetry/telemetry/browser_gone_exception.py
new file mode 100644
index 0000000..56bf012
--- /dev/null
+++ b/tools/telemetry/telemetry/browser_gone_exception.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+class BrowserGoneException(Exception):
+ """Represnets a crash of the entire browser.
+
+ In this state, all bets are pretty much off."""
+ pass
+
diff --git a/tools/telemetry/telemetry/browser_options.py b/tools/telemetry/telemetry/browser_options.py
new file mode 100644
index 0000000..25eff1f
--- /dev/null
+++ b/tools/telemetry/telemetry/browser_options.py
@@ -0,0 +1,148 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import optparse
+import os
+import sys
+import shlex
+import logging
+
+from telemetry import browser_finder
+from telemetry import wpr_modes
+
+class BrowserOptions(optparse.Values):
+ """Options to be used for discovering and launching a browser."""
+
+ def __init__(self, browser_type=None):
+ optparse.Values.__init__(self)
+
+ self.browser_type = browser_type
+ self.browser_executable = None
+ self.chrome_root = None
+ self.android_device = None
+ self.cros_ssh_identity = None
+
+ self.dont_override_profile = False
+ self.extra_browser_args = []
+ self.show_stdout = False
+
+ self.cros_remote = None
+ self.wpr_mode = wpr_modes.WPR_OFF
+ self.wpr_make_javascript_deterministic = True
+
+ self.verbosity = 0
+
+ def Copy(self):
+ other = BrowserOptions()
+ other.__dict__.update(self.__dict__)
+ return other
+
+ def CreateParser(self, *args, **kwargs):
+ parser = optparse.OptionParser(*args, **kwargs)
+
+ # Selection group
+ group = optparse.OptionGroup(parser, 'Which browser to use')
+ group.add_option('--browser',
+ dest='browser_type',
+ default=None,
+ help='Browser type to run, '
+ 'in order of priority. Supported values: list,%s' %
+ browser_finder.ALL_BROWSER_TYPES)
+ group.add_option('--browser-executable',
+ dest='browser_executable',
+ help='The exact browser to run.')
+ group.add_option('--chrome-root',
+ dest='chrome_root',
+ help='Where to look for chrome builds.'
+ 'Defaults to searching parent dirs by default.')
+ group.add_option('--device',
+ dest='android_device',
+ help='The android device ID to use'
+ 'If not specified, only 0 or 1 connected devcies are supported.')
+ group.add_option(
+ '--remote',
+ dest='cros_remote',
+ default=os.getenv('REMOTE'),
+ help='The IP address of a remote ChromeOS device to use. ' +
+ 'Defaults to $REMOTE from environment variable if set.')
+ group.add_option('--identity',
+ dest='cros_ssh_identity',
+ default=None,
+ help='The identity file to use when ssh\'ing into the ChromeOS device')
+ parser.add_option_group(group)
+
+ # Browser options
+ group = optparse.OptionGroup(parser, 'Browser options')
+ group.add_option('--dont-override-profile', action='store_true',
+ dest='dont_override_profile',
+ help='Uses the regular user profile instead of a clean one')
+ group.add_option('--extra-browser-args',
+ dest='extra_browser_args_as_string',
+ help='Additional arguments to pass to the browser when it starts')
+ group.add_option('--show-stdout',
+ action='store_true',
+ help='When possible, will display the stdout of the process')
+ parser.add_option_group(group)
+
+ # Page set options
+ group = optparse.OptionGroup(parser, 'Page set options')
+ group.add_option('--record', action='store_const',
+ dest='wpr_mode', const=wpr_modes.WPR_RECORD,
+ help='Record to the page set archive')
+ group.add_option('--page-repeat', dest='page_repeat', default=1,
+ help='Number of times to repeat each individual ' +
+ 'page in the pageset before proceeding.')
+ group.add_option('--pageset-repeat', dest='pageset_repeat', default=1,
+ help='Number of times to repeat the entire pageset ' +
+ 'before finishing.')
+ group.add_option('--test-shuffle', action='store_true', dest='test_shuffle',
+ help='Shuffle the order of pages within a pageset.')
+ group.add_option('--test-shuffle-order-file',
+ dest='test_shuffle_order_file', default=None,
+ help='Filename of an output of a previously run test on the current ' +
+ 'pageset. The tests will run in the same order again, overriding ' +
+ 'what is specified by --page-repeat and --pageset-repeat.')
+ parser.add_option_group(group)
+
+ # Debugging options
+ group = optparse.OptionGroup(parser, 'When things go wrong')
+ group.add_option(
+ '-v', '--verbose', action='count', dest='verbosity',
+ help='Increase verbosity level (repeat as needed)')
+ parser.add_option_group(group)
+
+ real_parse = parser.parse_args
+ def ParseArgs(args=None):
+ defaults = parser.get_default_values()
+ for k, v in defaults.__dict__.items():
+ if k in self.__dict__ and self.__dict__[k] != None:
+ continue
+ self.__dict__[k] = v
+ ret = real_parse(args, self) # pylint: disable=E1121
+
+ if self.verbosity >= 2:
+ logging.basicConfig(level=logging.DEBUG)
+ elif self.verbosity:
+ logging.basicConfig(level=logging.INFO)
+ else:
+ logging.basicConfig(level=logging.WARNING)
+
+ if self.browser_executable and not self.browser_type:
+ self.browser_type = 'exact'
+ if not self.browser_executable and not self.browser_type:
+ sys.stderr.write('Must provide --browser=<type>. ' +
+ 'Use --browser=list for valid options.\n')
+ sys.exit(1)
+ if self.browser_type == 'list':
+ types = browser_finder.GetAllAvailableBrowserTypes(self)
+ sys.stderr.write('Available browsers:\n')
+ sys.stdout.write(' %s\n' % '\n '.join(types))
+ sys.exit(1)
+ if self.extra_browser_args_as_string: # pylint: disable=E1101
+ tmp = shlex.split(
+ self.extra_browser_args_as_string) # pylint: disable=E1101
+ self.extra_browser_args.extend(tmp)
+ delattr(self, 'extra_browser_args_as_string')
+ return ret
+ parser.parse_args = ParseArgs
+ return parser
diff --git a/tools/telemetry/telemetry/browser_options_unittest.py b/tools/telemetry/telemetry/browser_options_unittest.py
new file mode 100644
index 0000000..c92c02a
--- /dev/null
+++ b/tools/telemetry/telemetry/browser_options_unittest.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+
+from telemetry import browser_options
+
+class BrowserOptionsTest(unittest.TestCase):
+ def testDefaults(self):
+ options = browser_options.BrowserOptions()
+ parser = options.CreateParser()
+ parser.add_option('-x', action='store', default=3)
+ parser.parse_args(['--browser', 'any'])
+ self.assertEquals(options.x, 3) # pylint: disable=E1101
+
+ def testDefaultsPlusOverride(self):
+ options = browser_options.BrowserOptions()
+ parser = options.CreateParser()
+ parser.add_option('-x', action='store', default=3)
+ parser.parse_args(['--browser', 'any', '-x', 10])
+ self.assertEquals(options.x, 10) # pylint: disable=E1101
+
+ def testDefaultsDontClobberPresetValue(self):
+ options = browser_options.BrowserOptions()
+ setattr(options, 'x', 7)
+ parser = options.CreateParser()
+ parser.add_option('-x', action='store', default=3)
+ parser.parse_args(['--browser', 'any'])
+ self.assertEquals(options.x, 7) # pylint: disable=E1101
+
+ def testCount0(self):
+ options = browser_options.BrowserOptions()
+ parser = options.CreateParser()
+ parser.add_option('-x', action='count', dest='v')
+ parser.parse_args(['--browser', 'any'])
+ self.assertEquals(options.v, None) # pylint: disable=E1101
+
+ def testCount2(self):
+ options = browser_options.BrowserOptions()
+ parser = options.CreateParser()
+ parser.add_option('-x', action='count', dest='v')
+ parser.parse_args(['--browser', 'any', '-xx'])
+ self.assertEquals(options.v, 2) # pylint: disable=E1101
+
+ def testOptparseMutabilityWhenSpecified(self):
+ options = browser_options.BrowserOptions()
+ parser = options.CreateParser()
+ parser.add_option('-x', dest='verbosity', action='store_true')
+ options_ret, _ = parser.parse_args(['--browser', 'any', '-x'])
+ self.assertEquals(options_ret, options)
+ self.assertTrue(options.verbosity)
+
+ def testOptparseMutabilityWhenNotSpecified(self):
+ options = browser_options.BrowserOptions()
+
+ parser = options.CreateParser()
+ parser.add_option('-x', dest='verbosity', action='store_true')
+ options_ret, _ = parser.parse_args(['--browser', 'any'])
+ self.assertEquals(options_ret, options)
+ self.assertFalse(options.verbosity)
diff --git a/tools/telemetry/telemetry/browser_unittest.py b/tools/telemetry/telemetry/browser_unittest.py
new file mode 100644
index 0000000..d49509e
--- /dev/null
+++ b/tools/telemetry/telemetry/browser_unittest.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+
+from telemetry import browser_finder
+from telemetry import options_for_unittests
+
+class BrowserTest(unittest.TestCase):
+ def testBrowserCreation(self):
+ options = options_for_unittests.Get()
+ browser_to_create = browser_finder.FindBrowser(options)
+ if not browser_to_create:
+ raise Exception('No browser found, cannot continue test.')
+ with browser_to_create.Create() as b:
+ self.assertEquals(1, b.num_tabs)
+
+ # Different browsers boot up to different things
+ assert b.GetNthTabUrl(0)
+
+ def testCommandLineOverriding(self):
+ # This test starts the browser with --enable-benchmarking, which should
+ # create a chrome.Interval namespace. This tests whether the command line is
+ # being set.
+ options = options_for_unittests.Get()
+
+ flag1 = '--user-agent=telemetry'
+ options.extra_browser_args.append(flag1)
+
+ browser_to_create = browser_finder.FindBrowser(options)
+ with browser_to_create.Create() as b:
+ with b.ConnectToNthTab(0) as t:
+ t.page.Navigate('http://www.google.com/')
+ t.WaitForDocumentReadyStateToBeInteractiveOrBetter()
+ self.assertEquals(t.runtime.Evaluate('navigator.userAgent'),
+ 'telemetry')
+
+ def testNewCloseTab(self):
+ options = options_for_unittests.Get()
+ browser_to_create = browser_finder.FindBrowser(options)
+ with browser_to_create.Create() as b:
+ self.assertEquals(1, b.num_tabs)
+ existing_tab_url = b.GetNthTabUrl(0)
+ b.NewTab()
+ self.assertEquals(2, b.num_tabs)
+ self.assertEquals(b.GetNthTabUrl(0), existing_tab_url)
+ self.assertEquals(b.GetNthTabUrl(1), 'about:blank')
+ b.CloseTab(1)
+ self.assertEquals(1, b.num_tabs)
+ self.assertEquals(b.GetNthTabUrl(0), existing_tab_url)
+ self.assertRaises(AssertionError, b.CloseTab, 0)
diff --git a/tools/telemetry/telemetry/cros_browser_backend.py b/tools/telemetry/telemetry/cros_browser_backend.py
new file mode 100644
index 0000000..e1d6c5b
--- /dev/null
+++ b/tools/telemetry/telemetry/cros_browser_backend.py
@@ -0,0 +1,180 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import logging
+import socket
+import subprocess
+import time
+
+from telemetry import browser_backend
+from telemetry import cros_interface
+
+class CrOSBrowserBackend(browser_backend.BrowserBackend):
+ """The backend for controlling a browser instance running on CrOS.
+ """
+ def __init__(self, browser_type, options, is_content_shell, cri):
+ super(CrOSBrowserBackend, self).__init__(is_content_shell, options)
+ # Initialize fields so that an explosion during init doesn't break in Close.
+ self._options = options
+ assert not is_content_shell
+ self._cri = cri
+ self._browser_type = browser_type
+
+ tmp = socket.socket()
+ tmp.bind(('', 0))
+ self._port = tmp.getsockname()[1]
+ tmp.close()
+
+ self._remote_debugging_port = self._cri.GetRemotePort()
+ self._tmpdir = None
+
+ self._X = None
+ self._proc = None
+
+ # TODO(nduca): Stop ui if running.
+ if self._cri.IsServiceRunning('ui'):
+ # Note, if this hangs, its probably because they were using wifi AND they
+ # had a user-specific wifi password, which when you stop ui kills the wifi
+ # connection.
+ logging.debug('stopping ui')
+ self._cri.GetCmdOutput(['stop', 'ui'])
+
+ # Set up user data dir.
+ if not is_content_shell:
+ logging.info('Preparing user data dir')
+ self._tmpdir = '/tmp/telemetry'
+ if options.dont_override_profile:
+ # TODO(nduca): Implement support for this.
+ logging.critical('Feature not (yet) implemented.')
+
+ # Ensure a clean user_data_dir.
+ self._cri.RmRF(self._tmpdir)
+
+ # Set startup args.
+ args = ['/opt/google/chrome/chrome']
+ args.extend(self.GetBrowserStartupArgs())
+
+ # Final bits of command line prep.
+ def EscapeIfNeeded(arg):
+ return arg.replace(' ', '" "')
+ args = [EscapeIfNeeded(arg) for arg in args]
+ prevent_output = not options.show_stdout
+
+ # Stop old X.
+ logging.info('Stoppping old X')
+ self._cri.KillAllMatching(
+ lambda name: name.startswith('/usr/bin/X '))
+
+ # Start X.
+ logging.info('Starting new X')
+ X_args = ['/usr/bin/X',
+ '-noreset',
+ '-nolisten',
+ 'tcp',
+ 'vt01',
+ '-auth',
+ '/var/run/chromelogin.auth']
+ self._X = cros_interface.DeviceSideProcess(
+ self._cri, X_args, prevent_output=prevent_output)
+
+ # Stop old chrome.
+ logging.info('Killing old chrome')
+ self._cri.KillAllMatching(
+ lambda name: name.startswith('/opt/google/chrome/chrome '))
+
+ # Start chrome via a bootstrap.
+ logging.info('Starting chrome')
+ self._proc = cros_interface.DeviceSideProcess(
+ self._cri,
+ args,
+ prevent_output=prevent_output,
+ extra_ssh_args=['-L%i:localhost:%i' % (
+ self._port, self._remote_debugging_port)],
+ leave_ssh_alive=True,
+ env={'DISPLAY': ':0',
+ 'USER': 'chronos'},
+ login_shell=True)
+
+ # You're done.
+ try:
+ self._WaitForBrowserToComeUp()
+ except:
+ import traceback
+ traceback.print_exc()
+ self.Close()
+ raise
+
+ def GetBrowserStartupArgs(self):
+ args = super(CrOSBrowserBackend, self).GetBrowserStartupArgs()
+
+ args.extend([
+ '--allow-webui-compositing',
+ '--aura-host-window-use-fullscreen',
+ '--enable-smooth-scrolling',
+ '--enable-threaded-compositing',
+ '--enable-per-tile-painting',
+ '--enable-gpu-sandboxing',
+ '--enable-accelerated-layers',
+ '--force-compositing-mode',
+ '--remote-debugging-port=%i' % self._remote_debugging_port,
+ '--start-maximized'])
+ if not self.is_content_shell:
+ args.append('--user-data-dir=%s' % self._tmpdir)
+
+ return args
+
+ def __del__(self):
+ self.Close()
+
+ def Close(self):
+ if self._proc:
+ self._proc.Close()
+ self._proc = None
+
+ if self._X:
+ self._X.Close()
+ self._X = None
+
+ if self._tmpdir:
+ self._cri.RmRF(self._tmpdir)
+ self._tmpdir = None
+
+ self._cri = None
+
+ def IsBrowserRunning(self):
+ if not self._proc:
+ return False
+ return self._proc.IsAlive()
+
+ def CreateForwarder(self, *ports):
+ assert self._cri
+ return SSHReverseForwarder(self._cri, *ports)
+
+
+class SSHReverseForwarder(object):
+ def __init__(self, cri, *ports):
+ self._proc = None
+ self._host_port = ports[0]
+
+ self._proc = subprocess.Popen(
+ cri.FormSSHCommandLine(['sleep', '99999999999'],
+ ['-R%i:localhost:%i' %
+ (port, port) for port in ports]),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ shell=False)
+
+ # TODO(nduca): How do we wait for the server to come up in a
+ # robust way?
+ time.sleep(1.5)
+
+ @property
+ def url(self):
+ assert self._proc
+ return 'http://localhost:%i' % self._host_port
+
+ def Close(self):
+ if self._proc:
+ self._proc.kill()
+ self._proc = None
diff --git a/tools/telemetry/telemetry/cros_browser_finder.py b/tools/telemetry/telemetry/cros_browser_finder.py
new file mode 100644
index 0000000..7c3b3f8
--- /dev/null
+++ b/tools/telemetry/telemetry/cros_browser_finder.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Finds android browsers that can be controlled by telemetry."""
+
+import logging
+
+from telemetry import browser
+from telemetry import platform
+from telemetry import possible_browser
+from telemetry import cros_browser_backend
+from telemetry import cros_interface
+
+ALL_BROWSER_TYPES = ','.join([
+ 'cros-chrome',
+ ])
+
+class PossibleCrOSBrowser(possible_browser.PossibleBrowser):
+ """A launchable android browser instance."""
+ def __init__(self, browser_type, options, *args):
+ super(PossibleCrOSBrowser, self).__init__(
+ browser_type, options)
+ self._args = args
+
+ def __repr__(self):
+ return 'PossibleCrOSBrowser(browser_type=%s)' % self.browser_type
+
+ def Create(self):
+ backend = cros_browser_backend.CrOSBrowserBackend(
+ self.browser_type, self._options, *self._args)
+ return browser.Browser(backend, platform.Platform())
+
+def FindAllAvailableBrowsers(options):
+ """Finds all the desktop browsers available on this machine."""
+ if options.cros_remote == None:
+ logging.debug('No --remote specified, will not probe for CrOS.')
+ return []
+
+ if not cros_interface.HasSSH():
+ logging.debug('ssh not found. Cannot talk to CrOS devices.')
+ return []
+ cri = cros_interface.CrOSInterface(options.cros_remote,
+ options.cros_ssh_identity)
+
+ # Check ssh
+ try:
+ cri.TryLogin()
+ except cros_interface.LoginException, ex:
+ if isinstance(ex, cros_interface.KeylessLoginRequiredException):
+ logging.warn('Could not ssh into %s. Your device must be configured',
+ options.cros_remote)
+ logging.warn('to allow passwordless login as root.')
+ logging.warn('For a test-build device, pass this to your script:')
+ logging.warn(' --identity $(CHROMITE)/ssh_keys/id_testing:')
+ logging.warn('')
+ logging.warn('For a developer-mode device, the steps are:')
+ logging.warn(' - Ensure you have an id_rsa.pub (etc) on this computer')
+ logging.warn(' - On the chromebook:')
+ logging.warn(' - Control-Alt-T; shell; sudo -s')
+ logging.warn(' - openssh-server start')
+ logging.warn(' - scp <this machine>:.ssh/id_rsa.pub /tmp/')
+ logging.warn(' - mkdir /root/.ssh')
+ logging.warn(' - chown go-rx /root/.ssh')
+ logging.warn(' - cat /tmp/id_rsa.pub >> /root/.ssh/authorized_keys')
+ logging.warn(' - chown 0600 /root/.ssh/authorized_keys')
+ logging.warn('There, that was easy!')
+ logging.warn('')
+ logging.warn('P.S. Please, tell your manager how INANE this is.')
+ else:
+ logging.warn(str(ex))
+ return []
+
+ if not cri.FileExistsOnDevice('/opt/google/chrome/chrome'):
+ logging.warn('Could not find a chrome on ' % cri.hostname)
+
+ return [PossibleCrOSBrowser('cros-chrome', options, False, cri)]
diff --git a/tools/telemetry/telemetry/cros_browser_finder_unittest.py b/tools/telemetry/telemetry/cros_browser_finder_unittest.py
new file mode 100644
index 0000000..07f2e03
--- /dev/null
+++ b/tools/telemetry/telemetry/cros_browser_finder_unittest.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# TODO(nduca): Add basic unit test for cros_browser_finder.
+#
+# Here, we should mock the cros_interface module (assuming its working) and
+# verify that the finder does the right thing. Because the finder delegates most
+# of its work to the CRI, the test code here is going to be comparatively
+# simple.
+
diff --git a/tools/telemetry/telemetry/cros_interface.py b/tools/telemetry/telemetry/cros_interface.py
new file mode 100644
index 0000000..5e3232c
--- /dev/null
+++ b/tools/telemetry/telemetry/cros_interface.py
@@ -0,0 +1,375 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""A wrapper around ssh for common operations on a CrOS-based device"""
+import logging
+import os
+import re
+import subprocess
+import sys
+import time
+import tempfile
+
+from telemetry import util
+
+_next_remote_port = 9224
+
+# TODO(nduca): This whole file is built up around making individual ssh calls
+# for each operation. It really could get away with a single ssh session built
+# around pexpect, I suspect, if we wanted it to be faster. But, this was
+# convenient.
+
+def RunCmd(args, cwd=None, quiet=False):
+ """Opens a subprocess to execute a program and returns its return value.
+
+ Args:
+ args: A string or a sequence of program arguments. The program to execute is
+ the string or the first item in the args sequence.
+ cwd: If not None, the subprocess's current directory will be changed to
+ |cwd| before it's executed.
+
+ Returns:
+ Return code from the command execution.
+ """
+ if not quiet:
+ logging.debug(' '.join(args) + ' ' + (cwd or ''))
+ with open(os.devnull, 'w') as devnull:
+ p = subprocess.Popen(args=args, cwd=cwd, stdout=devnull,
+ stderr=devnull, stdin=devnull, shell=False)
+ return p.wait()
+
+def GetAllCmdOutput(args, cwd=None, quiet=False):
+ """Open a subprocess to execute a program and returns its output.
+
+ Args:
+ args: A string or a sequence of program arguments. The program to execute is
+ the string or the first item in the args sequence.
+ cwd: If not None, the subprocess's current directory will be changed to
+ |cwd| before it's executed.
+
+ Returns:
+ Captures and returns the command's stdout.
+ Prints the command's stderr to logger (which defaults to stdout).
+ """
+ if not quiet:
+ logging.debug(' '.join(args) + ' ' + (cwd or ''))
+ with open(os.devnull, 'w') as devnull:
+ p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, stdin=devnull, shell=False)
+ stdout, stderr = p.communicate()
+ if not quiet:
+ logging.debug(' > stdout=[%s], stderr=[%s]', stdout, stderr)
+ return stdout, stderr
+
+class DeviceSideProcess(object):
+ def __init__(self,
+ cri,
+ device_side_args,
+ prevent_output=True,
+ extra_ssh_args=None,
+ leave_ssh_alive=False,
+ env=None,
+ login_shell=False):
+
+ # Init members first so that Close will always succeed.
+ self._cri = cri
+ self._proc = None
+ self._devnull = open(os.devnull, 'w')
+
+ if prevent_output:
+ out = self._devnull
+ else:
+ out = sys.stderr
+
+ cri.RmRF('/tmp/cros_interface_remote_device_pid')
+ cmd_str = ' '.join(device_side_args)
+ if env:
+ env_str = ' '.join(['%s=%s' % (k, v) for k, v in env.items()])
+ cmd = env_str + ' ' + cmd_str
+ else:
+ cmd = cmd_str
+ contents = """%s&\n""" % cmd
+ contents += 'echo $! > /tmp/cros_interface_remote_device_pid\n'
+ cri.PushContents(contents, '/tmp/cros_interface_remote_device_bootstrap.sh')
+
+ cmdline = ['/bin/bash']
+ if login_shell:
+ cmdline.append('-l')
+ cmdline.append('/tmp/cros_interface_remote_device_bootstrap.sh')
+ proc = subprocess.Popen(
+ cri.FormSSHCommandLine(cmdline,
+ extra_ssh_args=extra_ssh_args),
+ stdout=out,
+ stderr=out,
+ stdin=self._devnull,
+ shell=False)
+
+ time.sleep(0.1)
+ def TryGetResult():
+ try:
+ self._pid = cri.GetFileContents(
+ '/tmp/cros_interface_remote_device_pid').strip()
+ return True
+ except OSError:
+ return False
+ try:
+ util.WaitFor(TryGetResult, 5)
+ except util.TimeoutException:
+ raise Exception('Something horrible has happened!')
+
+ # Killing the ssh session leaves the process running. We dont
+ # need it anymore, unless we have port-forwards.
+ if not leave_ssh_alive:
+ proc.kill()
+ else:
+ self._proc = proc
+
+ self._pid = int(self._pid)
+ if not self.IsAlive():
+ raise OSError('Process did not come up or did not stay alive verry long!')
+ self._cri = cri
+
+ def Close(self, try_sigint_first=False):
+ if self.IsAlive():
+ # Try to politely shutdown, first.
+ if try_sigint_first:
+ logging.debug("kill -INT %i" % self._pid)
+ self._cri.GetAllCmdOutput(
+ ['kill', '-INT', str(self._pid)], quiet=True)
+ try:
+ self.Wait(timeout=0.5)
+ except util.TimeoutException:
+ pass
+
+ if self.IsAlive():
+ logging.debug("kill -KILL %i" % self._pid)
+ self._cri.GetAllCmdOutput(
+ ['kill', '-KILL', str(self._pid)], quiet=True)
+ try:
+ self.Wait(timeout=5)
+ except util.TimeoutException:
+ pass
+
+ if self.IsAlive():
+ raise Exception('Could not shutdown the process.')
+
+ self._cri = None
+ if self._proc:
+ self._proc.kill()
+ self._proc = None
+
+ if self._devnull:
+ self._devnull.close()
+ self._devnull = None
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.Close()
+ return
+
+ def Wait(self, timeout=1):
+ if not self._pid:
+ raise Exception('Closed')
+ def IsDone():
+ return not self.IsAlive()
+ util.WaitFor(IsDone, timeout)
+ self._pid = None
+
+ def IsAlive(self, quiet=True):
+ if not self._pid:
+ return False
+ exists = self._cri.FileExistsOnDevice('/proc/%i/cmdline' % self._pid,
+ quiet=quiet)
+ return exists
+
+def HasSSH():
+ try:
+ RunCmd(['ssh'], quiet=True)
+ RunCmd(['scp'], quiet=True)
+ logging.debug("HasSSH()->True")
+ return True
+ except OSError:
+ logging.debug("HasSSH()->False")
+ return False
+
+class LoginException(Exception):
+ pass
+
+class KeylessLoginRequiredException(LoginException):
+ pass
+
+class CrOSInterface(object):
+ # pylint: disable=R0923
+ def __init__(self, hostname, ssh_identity = None):
+ self._hostname = hostname
+ self._ssh_identity = None
+
+ if ssh_identity:
+ self._ssh_identity = os.path.abspath(os.path.expanduser(ssh_identity))
+
+ @property
+ def hostname(self):
+ return self._hostname
+
+ def FormSSHCommandLine(self, args, extra_ssh_args=None):
+ full_args = ['ssh',
+ '-o ConnectTimeout=5',
+ '-o ForwardX11=no',
+ '-o ForwardX11Trusted=no',
+ '-o StrictHostKeyChecking=yes',
+ '-o KbdInteractiveAuthentication=no',
+ '-o PreferredAuthentications=publickey',
+ '-n']
+ if self._ssh_identity is not None:
+ full_args.extend(['-i', self._ssh_identity])
+ if extra_ssh_args:
+ full_args.extend(extra_ssh_args)
+ full_args.append('root@%s' % self._hostname)
+ full_args.extend(args)
+ return full_args
+
+ def GetAllCmdOutput(self, args, cwd=None, quiet=False):
+ return GetAllCmdOutput(self.FormSSHCommandLine(args), cwd, quiet=quiet)
+
+ def TryLogin(self):
+ logging.debug('TryLogin()')
+ stdout, stderr = self.GetAllCmdOutput(['echo', '$USER'], quiet=True)
+
+ if stderr != '':
+ if 'Host key verification failed' in stderr:
+ raise LoginException(('%s host key verification failed. ' +
+ 'SSH to it manually to fix connectivity.') %
+ self._hostname)
+ if 'Operation timed out' in stderr:
+ raise LoginException('Timed out while logging into %s' % self._hostname)
+ if 'UNPROTECTED PRIVATE KEY FILE!' in stderr:
+ raise LoginException('Permissions for %s are too open. To fix this,\n'
+ 'chmod 600 %s' % (self._ssh_identity,
+ self._ssh_identity))
+ if 'Permission denied (publickey,keyboard-interactive)' in stderr:
+ raise KeylessLoginRequiredException(
+ 'Need to set up ssh auth for %s' % self._hostname)
+ raise LoginException('While logging into %s, got %s' % (
+ self._hostname, stderr))
+ if stdout != 'root\n':
+ raise LoginException(
+ 'Logged into %s, expected $USER=root, but got %s.' % (
+ self._hostname, stdout))
+
+ def FileExistsOnDevice(self, file_name, quiet=False):
+ stdout, stderr = self.GetAllCmdOutput([
+ 'if', 'test', '-a', file_name, ';',
+ 'then', 'echo', '1', ';',
+ 'fi'
+ ], quiet=True)
+ if stderr != '':
+ if "Connection timed out" in stderr:
+ raise OSError('Machine wasn\'t responding to ssh: %s' %
+ stderr)
+ raise OSError('Unepected error: %s' % stderr)
+ exists = stdout == '1\n'
+ if not quiet:
+ logging.debug("FileExistsOnDevice(<text>, %s)->%s" % (
+ file_name, exists))
+ return exists
+
+ def PushContents(self, text, remote_filename):
+ logging.debug("PushContents(<text>, %s)" % remote_filename)
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(text)
+ f.flush()
+ args = ['scp',
+ '-o ConnectTimeout=5',
+ '-o KbdInteractiveAuthentication=no',
+ '-o PreferredAuthentications=publickey',
+ '-o StrictHostKeyChecking=yes' ]
+
+ if self._ssh_identity:
+ args.extend(['-i', self._ssh_identity])
+
+ args.extend([os.path.abspath(f.name),
+ 'root@%s:%s' % (self._hostname, remote_filename)])
+
+ stdout, stderr = GetAllCmdOutput(args, quiet=True)
+ if stderr != '':
+ assert 'No such file or directory' in stderr
+ raise OSError
+
+ def GetFileContents(self, filename):
+ with tempfile.NamedTemporaryFile() as f:
+ args = ['scp',
+ '-o ConnectTimeout=5',
+ '-o KbdInteractiveAuthentication=no',
+ '-o PreferredAuthentications=publickey',
+ '-o StrictHostKeyChecking=yes' ]
+
+ if self._ssh_identity:
+ args.extend(['-i', self._ssh_identity])
+
+ args.extend(['root@%s:%s' % (self._hostname, filename),
+ os.path.abspath(f.name)])
+
+ stdout, stderr = GetAllCmdOutput(args, quiet=True)
+
+ if stderr != '':
+ assert 'No such file or directory' in stderr
+ raise OSError
+
+ with open(f.name, 'r') as f2:
+ res = f2.read()
+ logging.debug("GetFileContents(%s)->%s" % (filename, res))
+ return res
+
+ def ListProcesses(self):
+ stdout, stderr = self.GetAllCmdOutput([
+ '/bin/ps', '--no-headers',
+ '-A',
+ '-o', 'pid,args'], quiet=True)
+ assert stderr == ''
+ procs = []
+ for l in stdout.split('\n'): # pylint: disable=E1103
+ if l == '':
+ continue
+ m = re.match('^\s*(\d+)\s+(.+)', l, re.DOTALL)
+ assert m
+ procs.append(m.groups())
+ logging.debug("ListProcesses(<predicate>)->[%i processes]" % len(procs))
+ return procs
+
+ def RmRF(self, filename):
+ logging.debug("rm -rf %s" % filename)
+ self.GetCmdOutput(['rm', '-rf', filename], quiet=True)
+
+ def KillAllMatching(self, predicate):
+ kills = ['kill', '-KILL']
+ for p in self.ListProcesses():
+ if predicate(p[1]):
+ logging.info('Killing %s', repr(p))
+ kills.append(p[0])
+ logging.debug("KillAllMatching(<predicate>)->%i" % (len(kills) - 2))
+ if len(kills) > 2:
+ self.GetCmdOutput(kills, quiet=True)
+ return len(kills) - 2
+
+ def IsServiceRunning(self, service_name):
+ stdout, stderr = self.GetAllCmdOutput([
+ 'status', service_name], quiet=True)
+ assert stderr == ''
+ running = 'running, process' in stdout
+ logging.debug("IsServiceRunning(%s)->%s" % (service_name, running))
+ return running
+
+ def GetCmdOutput(self, args, quiet=False):
+ stdout, stderr = self.GetAllCmdOutput(args, quiet=True)
+ assert stderr == ''
+ if not quiet:
+ logging.debug("GetCmdOutput(%s)->%s" % (repr(args), stdout))
+ return stdout
+
+ def GetRemotePort(self):
+ global _next_remote_port
+ port = _next_remote_port
+ _next_remote_port += 1
+ return port
diff --git a/tools/telemetry/telemetry/cros_interface_unittest.py b/tools/telemetry/telemetry/cros_interface_unittest.py
new file mode 100644
index 0000000..a445a5c
--- /dev/null
+++ b/tools/telemetry/telemetry/cros_interface_unittest.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# TODO(nduca): Rewrite what some of these tests to use mocks instead of
+# actually talking to the device. This would improve our coverage quite
+# a bit.
+import unittest
+
+from telemetry import cros_interface
+from telemetry import options_for_unittests
+from telemetry import run_tests
+
+class CrOSInterfaceTest(unittest.TestCase):
+ @run_tests.RequiresBrowserOfType('cros-chrome')
+ def testDeviceSideProcessFailureToLaunch(self):
+ remote = options_for_unittests.Get().cros_remote
+ cri = cros_interface.CrOSInterface(
+ remote,
+ options_for_unittests.Get().cros_ssh_identity)
+
+ def WillFail():
+ dsp = cros_interface.DeviceSideProcess(
+ cri,
+ ['sfsdfskjflwejfweoij'])
+ dsp.Close()
+ self.assertRaises(OSError, WillFail)
+
+ @run_tests.RequiresBrowserOfType('cros-chrome')
+ def testDeviceSideProcessCloseDoesClose(self):
+ remote = options_for_unittests.Get().cros_remote
+ cri = cros_interface.CrOSInterface(
+ remote,
+ options_for_unittests.Get().cros_ssh_identity)
+
+ with cros_interface.DeviceSideProcess(
+ cri,
+ ['sleep', '111']) as dsp:
+ procs = cri.ListProcesses()
+ sleeps = [x for x in procs
+ if x[1] == 'sleep 111']
+ assert dsp.IsAlive()
+ procs = cri.ListProcesses()
+ sleeps = [x for x in procs
+ if x[1] == 'sleep 111']
+ self.assertEquals(len(sleeps), 0)
+
+ @run_tests.RequiresBrowserOfType('cros-chrome')
+ def testPushContents(self):
+ remote = options_for_unittests.Get().cros_remote
+ cri = cros_interface.CrOSInterface(
+ remote,
+ options_for_unittests.Get().cros_ssh_identity)
+ cri.GetCmdOutput(['rm', '-rf', '/tmp/testPushContents'])
+ cri.PushContents('hello world', '/tmp/testPushContents')
+ contents = cri.GetFileContents('/tmp/testPushContents')
+ self.assertEquals(contents, 'hello world')
+
+ @run_tests.RequiresBrowserOfType('cros-chrome')
+ def testExists(self):
+ remote = options_for_unittests.Get().cros_remote
+ cri = cros_interface.CrOSInterface(
+ remote,
+ options_for_unittests.Get().cros_ssh_identity)
+ self.assertTrue(cri.FileExistsOnDevice('/proc/cpuinfo'))
+ self.assertTrue(cri.FileExistsOnDevice('/etc/passwd'))
+ self.assertFalse(cri.FileExistsOnDevice('/etc/sdlfsdjflskfjsflj'))
+
+ @run_tests.RequiresBrowserOfType('cros-chrome')
+ def testGetFileContents(self): # pylint: disable=R0201
+ remote = options_for_unittests.Get().cros_remote
+ cri = cros_interface.CrOSInterface(
+ remote,
+ options_for_unittests.Get().cros_ssh_identity)
+ hosts = cri.GetFileContents('/etc/hosts')
+ assert hosts.startswith('# /etc/hosts')
+
+ @run_tests.RequiresBrowserOfType('cros-chrome')
+ def testGetFileContentsForSomethingThatDoesntExist(self):
+ remote = options_for_unittests.Get().cros_remote
+ cri = cros_interface.CrOSInterface(
+ remote,
+ options_for_unittests.Get().cros_ssh_identity)
+ self.assertRaises(
+ OSError,
+ lambda: cri.GetFileContents('/tmp/209fuslfskjf/dfsfsf'))
+
+ @run_tests.RequiresBrowserOfType('cros-chrome')
+ def testListProcesses(self): # pylint: disable=R0201
+ remote = options_for_unittests.Get().cros_remote
+ cri = cros_interface.CrOSInterface(
+ remote,
+ options_for_unittests.Get().cros_ssh_identity)
+ with cros_interface.DeviceSideProcess(
+ cri,
+ ['sleep', '11']):
+ procs = cri.ListProcesses()
+ sleeps = [x for x in procs
+ if x[1] == 'sleep 11']
+
+ assert len(sleeps) == 1
+
+ @run_tests.RequiresBrowserOfType('cros-chrome')
+ def testIsServiceRunning(self):
+ remote = options_for_unittests.Get().cros_remote
+ cri = cros_interface.CrOSInterface(
+ remote,
+ options_for_unittests.Get().cros_ssh_identity)
+
+ self.assertTrue(cri.IsServiceRunning('openssh-server'))
+
diff --git a/tools/telemetry/telemetry/desktop_browser_backend.py b/tools/telemetry/telemetry/desktop_browser_backend.py
new file mode 100644
index 0000000..5d20c5d
--- /dev/null
+++ b/tools/telemetry/telemetry/desktop_browser_backend.py
@@ -0,0 +1,110 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os as os
+import subprocess as subprocess
+import shutil
+import tempfile
+
+from telemetry import browser_backend
+from telemetry import util
+
+DEFAULT_PORT = 9273
+
+class DesktopBrowserBackend(browser_backend.BrowserBackend):
+ """The backend for controlling a locally-executed browser instance, on Linux,
+ Mac or Windows.
+ """
+ def __init__(self, options, executable, is_content_shell):
+ super(DesktopBrowserBackend, self).__init__(is_content_shell, options)
+
+ # Initialize fields so that an explosion during init doesn't break in Close.
+ self._proc = None
+ self._devnull = None
+ self._tmpdir = None
+
+ self._executable = executable
+ if not self._executable:
+ raise Exception('Cannot create browser, no executable found!')
+
+ self._port = DEFAULT_PORT
+ args = [self._executable]
+ args.extend(self.GetBrowserStartupArgs())
+ if not options.show_stdout:
+ self._devnull = open(os.devnull, 'w')
+ self._proc = subprocess.Popen(
+ args, stdout=self._devnull, stderr=self._devnull)
+ else:
+ self._devnull = None
+ self._proc = subprocess.Popen(args)
+
+ try:
+ self._WaitForBrowserToComeUp()
+ except:
+ self.Close()
+ raise
+
+ def GetBrowserStartupArgs(self):
+ args = super(DesktopBrowserBackend, self).GetBrowserStartupArgs()
+ args.append('--remote-debugging-port=%i' % self._port)
+ args.append('--window-size=1280,1024')
+ args.append('--enable-benchmarking')
+ if not self.options.dont_override_profile:
+ self._tmpdir = tempfile.mkdtemp()
+ args.append('--user-data-dir=%s' % self._tmpdir)
+ return args
+
+ def IsBrowserRunning(self):
+ return self._proc.poll() == None
+
+ def __del__(self):
+ self.Close()
+
+ def Close(self):
+ if self._proc:
+
+ def IsClosed():
+ if not self._proc:
+ return True
+ return self._proc.poll() != None
+
+ # Try to politely shutdown, first.
+ self._proc.terminate()
+ try:
+ util.WaitFor(IsClosed, timeout=1)
+ self._proc = None
+ except util.TimeoutException:
+ pass
+
+ # Kill it.
+ if not IsClosed():
+ self._proc.kill()
+ try:
+ util.WaitFor(IsClosed, timeout=5)
+ self._proc = None
+ except util.TimeoutException:
+ self._proc = None
+ raise Exception('Could not shutdown the browser.')
+
+ if self._tmpdir and os.path.exists(self._tmpdir):
+ shutil.rmtree(self._tmpdir, ignore_errors=True)
+ self._tmpdir = None
+
+ if self._devnull:
+ self._devnull.close()
+ self._devnull = None
+
+ def CreateForwarder(self, *ports):
+ return DoNothingForwarder(ports[0])
+
+class DoNothingForwarder(object):
+ def __init__(self, host_port):
+ self._host_port = host_port
+
+ @property
+ def url(self):
+ assert self._host_port
+ return 'http://localhost:%i' % self._host_port
+
+ def Close(self):
+ self._host_port = None
diff --git a/tools/telemetry/telemetry/desktop_browser_finder.py b/tools/telemetry/telemetry/desktop_browser_finder.py
new file mode 100644
index 0000000..15c630d
--- /dev/null
+++ b/tools/telemetry/telemetry/desktop_browser_finder.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Finds desktop browsers that can be controlled by telemetry."""
+
+import logging
+import os
+import subprocess
+import sys
+
+from telemetry import browser
+from telemetry import desktop_browser_backend
+from telemetry import platform
+from telemetry import possible_browser
+
+ALL_BROWSER_TYPES = ','.join([
+ 'exact',
+ 'release',
+ 'debug',
+ 'canary',
+ 'content-shell-debug',
+ 'content-shell-release',
+ 'system'])
+
+class PossibleDesktopBrowser(possible_browser.PossibleBrowser):
+ """A desktop browser that can be controlled."""
+
+ def __init__(self, browser_type, options, executable, is_content_shell):
+ super(PossibleDesktopBrowser, self).__init__(browser_type, options)
+ self._local_executable = executable
+ self._is_content_shell = is_content_shell
+
+ def __repr__(self):
+ return 'PossibleDesktopBrowser(browser_type=%s)' % self.browser_type
+
+ def Create(self):
+ backend = desktop_browser_backend.DesktopBrowserBackend(
+ self._options, self._local_executable, self._is_content_shell)
+ return browser.Browser(backend, platform.Platform())
+
+def FindAllAvailableBrowsers(options):
+ """Finds all the desktop browsers available on this machine."""
+ browsers = []
+
+ has_display = True
+ if (sys.platform.startswith('linux') and
+ os.getenv('DISPLAY') == None):
+ has_display = False
+
+ # Add the explicit browser executable if given.
+ if options.browser_executable:
+ if os.path.exists(options.browser_executable):
+ browsers.append(PossibleDesktopBrowser('exact', options,
+ options.browser_executable, False))
+
+ # Look for a browser in the standard chrome build locations.
+ if options.chrome_root:
+ chrome_root = options.chrome_root
+ else:
+ chrome_root = os.path.join(os.path.dirname(__file__), '..', '..', '..')
+
+ if sys.platform == 'darwin':
+ chromium_app_name = 'Chromium.app/Contents/MacOS/Chromium'
+ content_shell_app_name = 'Content Shell.app/Contents/MacOS/Content Shell'
+ elif sys.platform.startswith('linux'):
+ chromium_app_name = 'chrome'
+ content_shell_app_name = 'content_shell'
+ elif sys.platform.startswith('win'):
+ chromium_app_name = 'chrome.exe'
+ content_shell_app_name = 'content_shell.exe'
+ else:
+ raise Exception('Platform not recognized')
+
+ build_dirs = ['build',
+ 'out',
+ 'sconsbuild',
+ 'xcodebuild']
+
+ def AddIfFound(browser_type, type_dir, app_name, content_shell):
+ for build_dir in build_dirs:
+ app = os.path.join(chrome_root, build_dir, type_dir, app_name)
+ if os.path.exists(app):
+ browsers.append(PossibleDesktopBrowser(browser_type, options,
+ app, content_shell))
+ return True
+ return False
+
+ # Add local builds
+ AddIfFound('debug', 'Debug', chromium_app_name, False)
+ AddIfFound('content-shell-debug', 'Debug', content_shell_app_name, True)
+ AddIfFound('release', 'Release', chromium_app_name, False)
+ AddIfFound('content-shell-release', 'Release', content_shell_app_name, True)
+
+ # Mac-specific options.
+ if sys.platform == 'darwin':
+ mac_canary = ('/Applications/Google Chrome Canary.app/'
+ 'Contents/MacOS/Google Chrome Canary')
+ mac_system = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
+ if os.path.exists(mac_canary):
+ browsers.append(PossibleDesktopBrowser('canary', options,
+ mac_canary, False))
+
+ if os.path.exists(mac_system):
+ browsers.append(PossibleDesktopBrowser('system', options,
+ mac_system, False))
+
+ # Linux specific options.
+ if sys.platform.startswith('linux'):
+ # Look for a google-chrome instance.
+ found = False
+ try:
+ with open(os.devnull, 'w') as devnull:
+ found = subprocess.call(['google-chrome', '--version'],
+ stdout=devnull, stderr=devnull) == 0
+ except OSError:
+ pass
+ if found:
+ browsers.append(
+ PossibleDesktopBrowser('system', options,
+ 'google-chrome', False))
+
+ # Win32-specific options.
+ if sys.platform.startswith('win'):
+ system_path = os.path.join('Google', 'Chrome', 'Application')
+ canary_path = os.path.join('Google', 'Chrome SxS', 'Application')
+
+ win_search_paths = [os.getenv('PROGRAMFILES(X86)'),
+ os.getenv('PROGRAMFILES'),
+ os.getenv('LOCALAPPDATA')]
+
+ for path in win_search_paths:
+ if not path:
+ continue
+ if AddIfFound('canary', os.path.join(path, canary_path),
+ chromium_app_name, False):
+ break
+
+ for path in win_search_paths:
+ if not path:
+ continue
+ if AddIfFound('system', os.path.join(path, system_path),
+ chromium_app_name, False):
+ break
+
+ if len(browsers) and not has_display:
+ logging.warning(
+ 'Found (%s), but you do not have a DISPLAY environment set.' %
+ ','.join([b.browser_type for b in browsers]))
+ return []
+
+ return browsers
diff --git a/tools/telemetry/telemetry/desktop_browser_finder_unittest.py b/tools/telemetry/telemetry/desktop_browser_finder_unittest.py
new file mode 100644
index 0000000..52184cc
--- /dev/null
+++ b/tools/telemetry/telemetry/desktop_browser_finder_unittest.py
@@ -0,0 +1,188 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+
+from telemetry import browser_options
+from telemetry import desktop_browser_finder
+from telemetry import system_stub
+
+# This file verifies the logic for finding a browser instance on all platforms
+# at once. It does so by providing stubs for the OS/sys/subprocess primitives
+# that the underlying finding logic usually uses to locate a suitable browser.
+# We prefer this approach to having to run the same test on every platform on
+# which we want this code to work.
+
+class FindTestBase(unittest.TestCase):
+ def setUp(self):
+ self._options = browser_options.BrowserOptions()
+ self._options.chrome_root = '../../../'
+ self._stubs = system_stub.Override(desktop_browser_finder,
+ ['os', 'subprocess', 'sys'])
+
+ def tearDown(self):
+ self._stubs.Restore()
+
+ @property
+ def _files(self):
+ return self._stubs.os.path.files
+
+ def DoFindAll(self):
+ return desktop_browser_finder.FindAllAvailableBrowsers(self._options)
+
+ def DoFindAllTypes(self):
+ browsers = self.DoFindAll()
+ return [b.browser_type for b in browsers]
+
+def has_type(array, browser_type):
+ return len([x for x in array if x.browser_type == browser_type]) != 0
+
+class FindSystemTest(FindTestBase):
+ def setUp(self):
+ super(FindSystemTest, self).setUp()
+ self._stubs.sys.platform = 'win32'
+
+ def testFindProgramFiles(self):
+ self._files.append(
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe')
+ self._stubs.os.program_files = 'C:\\Program Files'
+ self.assertTrue('system' in self.DoFindAllTypes())
+
+ def testFindProgramFilesX86(self):
+ self._files.append(
+ 'C:\\Program Files(x86)\\Google\\Chrome\\Application\\chrome.exe')
+ self._stubs.os.program_files_x86 = 'C:\\Program Files(x86)'
+ self.assertTrue('system' in self.DoFindAllTypes())
+
+ def testFindLocalAppData(self):
+ self._files.append(
+ 'C:\\Local App Data\\Google\\Chrome\\Application\\chrome.exe')
+ self._stubs.os.local_app_data = 'C:\\Local App Data'
+ self.assertTrue('system' in self.DoFindAllTypes())
+
+class FindLocalBuildsTest(FindTestBase):
+ def setUp(self):
+ super(FindLocalBuildsTest, self).setUp()
+ self._stubs.sys.platform = 'win32'
+
+ def testFindBuild(self):
+ self._files.append('..\\..\\..\\build\\Release\\chrome.exe')
+ self.assertTrue('release' in self.DoFindAllTypes())
+
+ def testFindOut(self):
+ self._files.append('..\\..\\..\\out\\Release\\chrome.exe')
+ self.assertTrue('release' in self.DoFindAllTypes())
+
+ def testFindSconsbuild(self):
+ self._files.append('..\\..\\..\\sconsbuild\\Release\\chrome.exe')
+ self.assertTrue('release' in self.DoFindAllTypes())
+
+ def testFindXcodebuild(self):
+ self._files.append('..\\..\\..\\xcodebuild\\Release\\chrome.exe')
+ self.assertTrue('release' in self.DoFindAllTypes())
+
+class OSXFindTest(FindTestBase):
+ def setUp(self):
+ super(OSXFindTest, self).setUp()
+ self._stubs.sys.platform = 'darwin'
+ self._files.append('/Applications/Google Chrome Canary.app/'
+ 'Contents/MacOS/Google Chrome Canary')
+ self._files.append('/Applications/Google Chrome.app/' +
+ 'Contents/MacOS/Google Chrome')
+ self._files.append(
+ '../../../out/Release/Chromium.app/Contents/MacOS/Chromium')
+ self._files.append(
+ '../../../out/Debug/Chromium.app/Contents/MacOS/Chromium')
+ self._files.append(
+ '../../../out/Release/Content Shell.app/Contents/MacOS/Content Shell')
+ self._files.append(
+ '../../../out/Debug/Content Shell.app/Contents/MacOS/Content Shell')
+
+ def testFindAll(self):
+ types = self.DoFindAllTypes()
+ self.assertEquals(
+ set(types),
+ set(['debug', 'release',
+ 'content-shell-debug', 'content-shell-release',
+ 'canary', 'system']))
+
+
+class LinuxFindTest(FindTestBase):
+ def setUp(self):
+ super(LinuxFindTest, self).setUp()
+
+ self._stubs.sys.platform = 'linux2'
+ self._files.append('/foo/chrome')
+ self._files.append('../../../out/Release/chrome')
+ self._files.append('../../../out/Debug/chrome')
+ self._files.append('../../../out/Release/content_shell')
+ self._files.append('../../../out/Debug/content_shell')
+
+ self.has_google_chrome_on_path = False
+ this = self
+ def call_hook(*args, **kwargs): # pylint: disable=W0613
+ if this.has_google_chrome_on_path:
+ return 0
+ raise OSError('Not found')
+ self._stubs.subprocess.call = call_hook
+
+ def testFindAllWithExact(self):
+ types = self.DoFindAllTypes()
+ self.assertEquals(
+ set(types),
+ set(['debug', 'release',
+ 'content-shell-debug', 'content-shell-release']))
+
+ def testFindWithProvidedExecutable(self):
+ self._options.browser_executable = '/foo/chrome'
+ self.assertTrue('exact' in self.DoFindAllTypes())
+
+ def testFindUsingDefaults(self):
+ self.has_google_chrome_on_path = True
+ self.assertTrue('release' in self.DoFindAllTypes())
+
+ del self._files[1]
+ self.has_google_chrome_on_path = True
+ self.assertTrue('system' in self.DoFindAllTypes())
+
+ self.has_google_chrome_on_path = False
+ del self._files[1]
+ self.assertEquals(['content-shell-debug', 'content-shell-release'],
+ self.DoFindAllTypes())
+
+ def testFindUsingRelease(self):
+ self.assertTrue('release' in self.DoFindAllTypes())
+
+
+class WinFindTest(FindTestBase):
+ def setUp(self):
+ super(WinFindTest, self).setUp()
+
+ self._stubs.sys.platform = 'win32'
+ self._stubs.os.local_app_data = 'c:\\Users\\Someone\\AppData\\Local'
+ self._files.append('c:\\tmp\\chrome.exe')
+ self._files.append('..\\..\\..\\build\\Release\\chrome.exe')
+ self._files.append('..\\..\\..\\build\\Debug\\chrome.exe')
+ self._files.append('..\\..\\..\\build\\Release\\content_shell.exe')
+ self._files.append('..\\..\\..\\build\\Debug\\content_shell.exe')
+ self._files.append(self._stubs.os.local_app_data + '\\' +
+ 'Google\\Chrome\\Application\\chrome.exe')
+ self._files.append(self._stubs.os.local_app_data + '\\' +
+ 'Google\\Chrome SxS\\Application\\chrome.exe')
+
+ def testFindAllGivenDefaults(self):
+ types = self.DoFindAllTypes()
+ self.assertEquals(set(types),
+ set(['debug', 'release',
+ 'content-shell-debug', 'content-shell-release',
+ 'system', 'canary']))
+
+ def testFindAllWithExact(self):
+ self._options.browser_executable = 'c:\\tmp\\chrome.exe'
+ types = self.DoFindAllTypes()
+ self.assertEquals(
+ set(types),
+ set(['exact',
+ 'debug', 'release',
+ 'content-shell-debug', 'content-shell-release',
+ 'system', 'canary']))
diff --git a/tools/telemetry/telemetry/facebook_credentials_backend.py b/tools/telemetry/telemetry/facebook_credentials_backend.py
new file mode 100644
index 0000000..abde849
--- /dev/null
+++ b/tools/telemetry/telemetry/facebook_credentials_backend.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from telemetry import form_based_credentials_backend
+
+class FacebookCredentialsBackend(
+ form_based_credentials_backend.FormBasedCredentialsBackend):
+ @property
+ def credentials_type(self):
+ return 'facebook'
+
+ @property
+ def url(self):
+ return 'http://www.facebook.com/'
+
+ @property
+ def form_id(self):
+ return 'login_form'
+
+ @property
+ def login_input_id(self):
+ return 'email'
+
+ @property
+ def password_input_id(self):
+ return 'pass'
diff --git a/tools/telemetry/telemetry/form_based_credentials_backend.py b/tools/telemetry/telemetry/form_based_credentials_backend.py
new file mode 100644
index 0000000..6a21cb8
--- /dev/null
+++ b/tools/telemetry/telemetry/form_based_credentials_backend.py
@@ -0,0 +1,92 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+
+import telemetry
+from telemetry import util
+
+def _WaitForFormToLoad(form_id, tab):
+ def IsFormLoaded():
+ return tab.runtime.Evaluate(
+ 'document.querySelector("#%s")!== null' % form_id)
+
+ # Wait until the form is submitted and the page completes loading.
+ util.WaitFor(lambda: IsFormLoaded(), 60) # pylint: disable=W0108
+
+def _SubmitFormAndWait(form_id, tab):
+ js = 'document.getElementById("%s").submit();' % form_id
+ tab.runtime.Execute(js)
+
+ def IsLoginStillHappening():
+ return tab.runtime.Evaluate(
+ 'document.querySelector("#%s")!== null' % form_id)
+
+ # Wait until the form is submitted and the page completes loading.
+ util.WaitFor(lambda: not IsLoginStillHappening(), 60)
+
+class FormBasedCredentialsBackend(object):
+ def __init__(self):
+ self._logged_in = False
+
+ @property
+ def credentials_type(self):
+ raise NotImplementedError()
+
+ @property
+ def url(self):
+ raise NotImplementedError()
+
+ @property
+ def form_id(self):
+ raise NotImplementedError()
+
+ @property
+ def login_input_id(self):
+ raise NotImplementedError()
+
+ @property
+ def password_input_id(self):
+ raise NotImplementedError()
+
+ def LoginNeeded(self, tab, config):
+ """Logs in to a test account.
+
+ Raises:
+ RuntimeError: if could not get credential information.
+ """
+ if self._logged_in:
+ return True
+
+ if 'username' not in config or 'password' not in config:
+ message = ('Credentials for "%s" must include username and password.' %
+ self.credentials_type)
+ raise RuntimeError(message)
+
+ logging.debug('Logging into %s account...' % self.credentials_type)
+
+ try:
+ logging.info('Loading %s...', self.url)
+ tab.page.Navigate(self.url)
+ _WaitForFormToLoad(self.form_id, tab)
+ tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
+ logging.info('Loaded page: %s', self.url)
+
+ email_id = 'document.getElementById("%s").value = "%s"; ' % (
+ self.login_input_id, config['username'])
+ password = 'document.getElementById("%s").value = "%s"; ' % (
+ self.password_input_id, config['password'])
+ tab.runtime.Execute(email_id)
+ tab.runtime.Execute(password)
+
+ _SubmitFormAndWait(self.form_id, tab)
+
+ self._logged_in = True
+ return True
+ except telemetry.TimeoutException:
+ logging.warning('Timed out while loading: %s', self.url)
+ return False
+
+ def LoginNoLongerNeeded(self, tab): # pylint: disable=W0613
+ assert self._logged_in
diff --git a/tools/telemetry/telemetry/google_credentials_backend.py b/tools/telemetry/telemetry/google_credentials_backend.py
new file mode 100644
index 0000000..600dff9
--- /dev/null
+++ b/tools/telemetry/telemetry/google_credentials_backend.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from telemetry import form_based_credentials_backend
+
+class GoogleCredentialsBackend(
+ form_based_credentials_backend.FormBasedCredentialsBackend):
+ @property
+ def credentials_type(self):
+ return 'google'
+
+ @property
+ def url(self):
+ return 'https://accounts.google.com/'
+
+ @property
+ def form_id(self):
+ return 'gaia_loginform'
+
+ @property
+ def login_input_id(self):
+ return 'Email'
+
+ @property
+ def password_input_id(self):
+ return 'Passwd'
diff --git a/tools/telemetry/telemetry/google_credentials_backend_unittest.py b/tools/telemetry/telemetry/google_credentials_backend_unittest.py
new file mode 100644
index 0000000..5c2ed7a
--- /dev/null
+++ b/tools/telemetry/telemetry/google_credentials_backend_unittest.py
@@ -0,0 +1,67 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import unittest
+
+from telemetry import browser_finder
+from telemetry import google_credentials_backend
+from telemetry import simple_mock
+from telemetry import options_for_unittests
+
+_ = simple_mock.DONT_CARE
+
+class MockTab(simple_mock.MockObject):
+ def __init__(self):
+ super(MockTab, self).__init__()
+ self.runtime = simple_mock.MockObject(self)
+ self.page = simple_mock.MockObject(self)
+
+class TestGoogleCredentialsBackend(unittest.TestCase):
+ def testRealLoginIfPossible(self):
+ credentials_path = os.path.join(
+ os.path.dirname(__file__),
+ '..', '..', 'perf', 'data', 'credentials.json')
+ if not os.path.exists(credentials_path):
+ return
+
+ options = options_for_unittests.Get()
+ with browser_finder.FindBrowser(options).Create() as b:
+ b.credentials.credentials_path = credentials_path
+ if not b.credentials.CanLogin('google'):
+ return
+ with b.ConnectToNthTab(0) as tab:
+ ret = b.credentials.LoginNeeded(tab, 'google')
+ self.assertTrue(ret)
+
+ def testLoginUsingMock(self): # pylint: disable=R0201
+ tab = MockTab()
+
+ backend = google_credentials_backend.GoogleCredentialsBackend()
+ config = {'username': 'blah',
+ 'password': 'blargh'}
+
+ tab.page.ExpectCall('Navigate', 'https://accounts.google.com/')
+ tab.runtime.ExpectCall('Evaluate', _).WillReturn(False)
+ tab.runtime.ExpectCall('Evaluate', _).WillReturn(True)
+ tab.ExpectCall('WaitForDocumentReadyStateToBeInteractiveOrBetter')
+
+ def VerifyEmail(js):
+ assert 'Email' in js
+ assert 'blah' in js
+ tab.runtime.ExpectCall('Execute', _).WhenCalled(VerifyEmail)
+
+ def VerifyPw(js):
+ assert 'Passwd' in js
+ assert 'largh' in js
+ tab.runtime.ExpectCall('Execute', _).WhenCalled(VerifyPw)
+
+ def VerifySubmit(js):
+ assert '.submit' in js
+ tab.runtime.ExpectCall('Execute', _).WhenCalled(VerifySubmit)
+
+ # Checking for form still up.
+ tab.runtime.ExpectCall('Evaluate', _).WillReturn(False)
+
+ backend.LoginNeeded(tab, config)
+
diff --git a/tools/telemetry/telemetry/inspector_backend.py b/tools/telemetry/telemetry/inspector_backend.py
new file mode 100644
index 0000000..83b4841
--- /dev/null
+++ b/tools/telemetry/telemetry/inspector_backend.py
@@ -0,0 +1,124 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import json
+import logging
+import socket
+
+from telemetry import tab_crash_exception
+from telemetry import util
+from telemetry import websocket
+
+class InspectorException(Exception):
+ pass
+
+class InspectorBackend(object):
+ def __init__(self, backend, descriptor):
+ self._backend = backend
+ self._descriptor = descriptor
+ self._socket_url = descriptor['webSocketDebuggerUrl']
+ self._socket = websocket.create_connection(
+ descriptor['webSocketDebuggerUrl'])
+ self._next_request_id = 0
+ self._domain_handlers = {}
+ self._cur_socket_timeout = 0
+
+ def Close(self):
+ for _, handlers in self._domain_handlers.items():
+ _, will_close_handler = handlers
+ will_close_handler()
+ self._domain_handlers = {}
+ self._socket.close()
+ self._socket = None
+ self._backend = None
+
+ def DispatchNotifications(self, timeout=10):
+ self._SetTimeout(timeout)
+ try:
+ data = self._socket.recv()
+ except socket.error:
+ if self._backend.DoesDebuggerUrlExist(self._socket_url):
+ return
+ raise tab_crash_exception.TabCrashException()
+
+ res = json.loads(data)
+ logging.debug('got [%s]', data)
+ if 'method' not in res:
+ return
+
+ mname = res['method']
+ dot_pos = mname.find('.')
+ domain_name = mname[:dot_pos]
+ if domain_name in self._domain_handlers:
+ try:
+ self._domain_handlers[domain_name][0](res)
+ except Exception:
+ import traceback
+ traceback.print_exc()
+
+ def SendAndIgnoreResponse(self, req):
+ req['id'] = self._next_request_id
+ self._next_request_id += 1
+ data = json.dumps(req)
+ self._socket.send(data)
+ logging.debug('sent [%s]', data)
+
+ def _SetTimeout(self, timeout):
+ if self._cur_socket_timeout != timeout:
+ self._socket.settimeout(timeout)
+ self._cur_socket_timeout = timeout
+
+ def SyncRequest(self, req, timeout=10):
+ # TODO(nduca): Listen to the timeout argument
+ # pylint: disable=W0613
+ self._SetTimeout(timeout)
+ self.SendAndIgnoreResponse(req)
+
+ while True:
+ try:
+ data = self._socket.recv()
+ except socket.error:
+ if self._backend.DoesDebuggerUrlExist(self._socket_url):
+ raise util.TimeoutException(
+ "TimedOut waiting for reply. This is unusual.")
+ raise tab_crash_exception.TabCrashException()
+
+ res = json.loads(data)
+ logging.debug('got [%s]', data)
+ if 'method' in res:
+ mname = res['method']
+ dot_pos = mname.find('.')
+ domain_name = mname[:dot_pos]
+ if domain_name in self._domain_handlers:
+ try:
+ self._domain_handlers[domain_name][0](res)
+ except Exception:
+ import traceback
+ traceback.print_exc()
+ else:
+ logging.debug('Unhandled inspector mesage: %s', data)
+ continue
+
+ if res['id'] != req['id']:
+ logging.debug('Dropped reply: %s', json.dumps(res))
+ continue
+ return res
+
+ def RegisterDomain(self,
+ domain_name, notification_handler, will_close_handler):
+ """Registers a given domain for handling notification methods.
+
+ For example, given inspector_backend:
+ def OnConsoleNotification(msg):
+ if msg['method'] == 'Console.messageAdded':
+ print msg['params']['message']
+ return
+ def OnConsoleClose(self):
+ pass
+ inspector_backend.RegisterDomain('Console',
+ OnConsoleNotification, OnConsoleClose)
+ """
+ assert domain_name not in self._domain_handlers
+ self._domain_handlers[domain_name] = (notification_handler,
+ will_close_handler)
+
diff --git a/tools/telemetry/telemetry/inspector_console.py b/tools/telemetry/telemetry/inspector_console.py
new file mode 100644
index 0000000..b9ea024
--- /dev/null
+++ b/tools/telemetry/telemetry/inspector_console.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import json
+import logging
+
+class InspectorConsole(object):
+ def __init__(self, inspector_backend):
+ self._inspector_backend = inspector_backend
+ self._inspector_backend.RegisterDomain(
+ 'Console',
+ self._OnNotification,
+ self._OnClose)
+ self._message_output_stream = None
+ self._last_message = None
+ self._console_enabled = False
+
+ def _OnNotification(self, msg):
+ logging.debug('Notification: %s', json.dumps(msg, indent=2))
+ if msg['method'] == 'Console.messageAdded':
+ self._last_message = 'At %s:%i: %s' % (
+ msg['params']['message']['url'],
+ msg['params']['message']['line'],
+ msg['params']['message']['text'])
+ if self._message_output_stream:
+ self._message_output_stream.write(
+ '%s\n' % self._last_message)
+
+ elif msg['method'] == 'Console.messageRepeatCountUpdated':
+ if self._message_output_stream:
+ self._message_output_stream.write(
+ '%s\n' % self._last_message)
+
+ def _OnClose(self):
+ pass
+
+ @property
+ def MessageOutputStream(self):
+ return self._message_output_stream
+
+ @MessageOutputStream.setter
+ def MessageOutputStream(self, stream):
+ self._message_output_stream = stream
+ self._UpdateConsoleEnabledState()
+
+ def _UpdateConsoleEnabledState(self):
+ enabled = self._message_output_stream != None
+ if enabled == self._console_enabled:
+ return
+
+ if enabled:
+ method_name = 'enable'
+ else:
+ method_name = 'disable'
+ self._inspector_backend.SyncRequest({
+ 'method': 'Console.%s' % method_name
+ })
+ self._console_enabled = enabled
diff --git a/tools/telemetry/telemetry/inspector_console_unittest.py b/tools/telemetry/telemetry/inspector_console_unittest.py
new file mode 100644
index 0000000..5e73706
--- /dev/null
+++ b/tools/telemetry/telemetry/inspector_console_unittest.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import re
+import StringIO
+
+from telemetry import tab_test_case
+from telemetry import util
+
+class TabConsoleTest(tab_test_case.TabTestCase):
+ def testConsoleOutputStream(self):
+ unittest_data_dir = os.path.join(os.path.dirname(__file__),
+ '..', 'unittest_data')
+ self._browser.SetHTTPServerDirectory(unittest_data_dir)
+
+ stream = StringIO.StringIO()
+ self._tab.console.MessageOutputStream = stream
+
+ self._tab.page.Navigate(
+ self._browser.http_server.UrlOf('page_that_logs_to_console.html'))
+ self._tab.WaitForDocumentReadyStateToBeComplete()
+
+ initial = self._tab.runtime.Evaluate('window.__logCount')
+ def GotLog():
+ current = self._tab.runtime.Evaluate('window.__logCount')
+ return current > initial
+ util.WaitFor(GotLog, 5)
+
+ lines = [l for l in stream.getvalue().split('\n') if len(l)]
+
+ self.assertTrue(len(lines) >= 1)
+ for l in lines:
+ u_l = 'http://localhost:(\d+)/page_that_logs_to_console.html:9'
+ self.assertTrue(re.match('At %s: Hello, world' % u_l, l))
+
diff --git a/tools/telemetry/telemetry/inspector_page.py b/tools/telemetry/telemetry/inspector_page.py
new file mode 100644
index 0000000..a879dcf
--- /dev/null
+++ b/tools/telemetry/telemetry/inspector_page.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import json
+import logging
+
+from telemetry import util
+
+class InspectorPage(object):
+ def __init__(self, inspector_backend):
+ self._inspector_backend = inspector_backend
+ self._inspector_backend.RegisterDomain(
+ 'Page',
+ self._OnNotification,
+ self._OnClose)
+ self._pending_navigate_url = None
+
+ def _OnNotification(self, msg):
+ logging.debug('Notification: %s', json.dumps(msg, indent=2))
+ if msg['method'] == 'Page.frameNavigated' and self._pending_navigate_url:
+ url = msg['params']['frame']['url']
+ if not url == 'chrome://newtab/':
+ # Marks the navigation as complete and unblocks the navigate call.
+ self._pending_navigate_url = None
+
+ def _OnClose(self):
+ pass
+
+ def Navigate(self, url, timeout=60):
+ """Navigates to url"""
+ # Turn on notifications. We need them to get the Page.frameNavigated event.
+ request = {
+ 'method': 'Page.enable'
+ }
+ res = self._inspector_backend.SyncRequest(request, timeout)
+ assert len(res['result'].keys()) == 0
+
+ # Navigate the page. However, there seems to be a bug in chrome devtools
+ # protocol where the request id for this event gets held on the browser side
+ # pretty much indefinitely.
+ #
+ # So, instead of waiting for the event to actually complete, wait for the
+ # Page.frameNavigated event.
+ request = {
+ 'method': 'Page.navigate',
+ 'params': {
+ 'url': url,
+ }
+ }
+ res = self._inspector_backend.SendAndIgnoreResponse(request)
+
+ self._pending_navigate_url = url
+ def IsNavigationDone(time_left):
+ self._inspector_backend.DispatchNotifications(time_left)
+ return self._pending_navigate_url == None
+
+ util.WaitFor(IsNavigationDone, timeout, pass_time_left_to_func=True)
+
+ # Turn off notifications.
+ request = {
+ 'method': 'Page.disable'
+ }
+ res = self._inspector_backend.SyncRequest(request, timeout)
+ assert len(res['result'].keys()) == 0
+
diff --git a/tools/telemetry/telemetry/inspector_page_unittest.py b/tools/telemetry/telemetry/inspector_page_unittest.py
new file mode 100644
index 0000000..fee527a
--- /dev/null
+++ b/tools/telemetry/telemetry/inspector_page_unittest.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+from telemetry import tab_test_case
+
+class InspectorPageTest(tab_test_case.TabTestCase):
+ def testPageNavigateToNormalUrl(self):
+ self._tab.page.Navigate('http://www.google.com')
+ self._tab.WaitForDocumentReadyStateToBeComplete()
+
+ def testPageNavigateToUrlChanger(self):
+ # The Url that we actually load is http://www.youtube.com/.
+ self._tab.page.Navigate('http://youtube.com/')
+
+ self._tab.WaitForDocumentReadyStateToBeComplete()
+
+ def testPageNavigateToImpossibleURL(self):
+ self._tab.page.Navigate('http://23f09f0f9fsdflajsfaldfkj2f3f.com')
+ self._tab.WaitForDocumentReadyStateToBeComplete()
diff --git a/tools/telemetry/telemetry/inspector_runtime.py b/tools/telemetry/telemetry/inspector_runtime.py
new file mode 100644
index 0000000..4479b66
--- /dev/null
+++ b/tools/telemetry/telemetry/inspector_runtime.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+class EvaluateException(Exception):
+ pass
+
+class InspectorRuntime(object):
+ def __init__(self, inspector_backend):
+ self._inspector_backend = inspector_backend
+ self._inspector_backend.RegisterDomain(
+ 'Runtime',
+ self._OnNotification,
+ self._OnClose)
+
+ def _OnNotification(self, msg):
+ pass
+
+ def _OnClose(self):
+ pass
+
+ def Execute(self, expr, timeout=60):
+ """Executes expr in javascript. Does not return the result.
+
+ If the expression failed to evaluate, EvaluateException will be raised.
+ """
+ self.Evaluate(expr + '; 0;', timeout)
+
+ def Evaluate(self, expr, timeout=60):
+ """Evalutes expr in javascript and returns the JSONized result.
+
+ Consider using Execute for cases where the result of the expression is not
+ needed.
+
+ If evaluation throws in javascript, a python EvaluateException will
+ be raised.
+
+ If the result of the evaluation cannot be JSONized, then an
+ EvaluationException will be raised.
+ """
+ request = {
+ 'method': 'Runtime.evaluate',
+ 'params': {
+ 'expression': expr,
+ 'returnByValue': True
+ }
+ }
+ res = self._inspector_backend.SyncRequest(request, timeout)
+ if 'error' in res:
+ raise EvaluateException(res['error']['message'])
+
+ if 'wasThrown' in res['result'] and res['result']['wasThrown']:
+ # TODO(nduca): propagate stacks from javascript up to the python
+ # exception.
+ raise EvaluateException(res['result']['result']['description'])
+ if res['result']['result']['type'] == 'undefined':
+ return None
+ return res['result']['result']['value']
diff --git a/tools/telemetry/telemetry/inspector_runtime_unittest.py b/tools/telemetry/telemetry/inspector_runtime_unittest.py
new file mode 100644
index 0000000..96d089f
--- /dev/null
+++ b/tools/telemetry/telemetry/inspector_runtime_unittest.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+from telemetry import inspector_runtime
+from telemetry import tab_test_case
+
+class InspectorRuntimeTest(tab_test_case.TabTestCase):
+ def testRuntimeEvaluateSimple(self):
+ res = self._tab.runtime.Evaluate('1+1')
+ assert res == 2
+
+ def testRuntimeEvaluateThatFails(self):
+ self.assertRaises(inspector_runtime.EvaluateException,
+ lambda: self._tab.runtime.Evaluate('fsdfsdfsf'))
+
+ def testRuntimeEvaluateOfSomethingThatCantJSONize(self):
+
+ def test():
+ self._tab.runtime.Evaluate("""
+ var cur = {};
+ var root = {next: cur};
+ for (var i = 0; i < 1000; i++) {
+ next = {};
+ cur.next = next;
+ cur = next;
+ }
+ root;""")
+ self.assertRaises(inspector_runtime.EvaluateException, test)
+
+ def testRuntimeExecuteOfSomethingThatCantJSONize(self):
+ self._tab.runtime.Execute('window')
diff --git a/tools/telemetry/telemetry/multi_page_benchmark.py b/tools/telemetry/telemetry/multi_page_benchmark.py
new file mode 100644
index 0000000..a17a031
--- /dev/null
+++ b/tools/telemetry/telemetry/multi_page_benchmark.py
@@ -0,0 +1,178 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+from collections import defaultdict
+import os
+import sys
+
+from telemetry import page_test
+
+# Get build/android/pylib scripts into our path.
+# TODO(tonyg): Move perf_tests_helper.py to a common location.
+sys.path.append(
+ os.path.abspath(
+ os.path.join(os.path.dirname(__file__),
+ '../../../build/android/pylib')))
+# pylint: disable=F0401
+from perf_tests_helper import GeomMeanAndStdDevFromHistogram
+from perf_tests_helper import PrintPerfResult # pylint: disable=F0401
+
+
+def _Mean(l):
+ return float(sum(l)) / len(l) if len(l) > 0 else 0.0
+
+
+class MeasurementFailure(page_test.Failure):
+ """Exception that can be thrown from MeasurePage to indicate an undesired but
+ designed-for problem."""
+ pass
+
+
+class BenchmarkResults(page_test.PageTestResults):
+ def __init__(self):
+ super(BenchmarkResults, self).__init__()
+ self.results_summary = defaultdict(list)
+ self.page_results = []
+ self.field_names = None
+ self.field_units = {}
+ self.field_types = {}
+
+ self._page = None
+ self._page_values = {}
+
+ def WillMeasurePage(self, page):
+ self._page = page
+ self._page_values = {}
+
+ def Add(self, trace_name, units, value, chart_name=None, data_type='default'):
+ name = trace_name
+ if chart_name:
+ name = '%s.%s' % (chart_name, trace_name)
+ assert name not in self._page_values, 'Result names must be unique'
+ assert name != 'url', 'The name url cannot be used'
+ if self.field_names:
+ assert name in self.field_names, """MeasurePage returned inconsistent
+results! You must return the same dict keys every time."""
+ else:
+ self.field_units[name] = units
+ self.field_types[name] = data_type
+ self._page_values[name] = value
+
+ def DidMeasurePage(self):
+ assert self._page, 'Failed to call WillMeasurePage'
+
+ if not self.field_names:
+ self.field_names = self._page_values.keys()
+ self.field_names.sort()
+
+ self.page_results.append(self._page_values)
+ for name in self.field_names:
+ units = self.field_units[name]
+ data_type = self.field_types[name]
+ value = self._page_values[name]
+ self.results_summary[(name, units, data_type)].append(value)
+
+ def PrintSummary(self, trace_tag):
+ if self.page_failures:
+ return
+ for measurement_units_type, values in sorted(
+ self.results_summary.iteritems()):
+ measurement, units, data_type = measurement_units_type
+ if '.' in measurement:
+ measurement, trace = measurement.split('.', 1)
+ trace += (trace_tag or '')
+ else:
+ trace = measurement + (trace_tag or '')
+ PrintPerfResult(measurement, trace, values, units, data_type)
+
+
+class CsvBenchmarkResults(BenchmarkResults):
+ def __init__(self, results_writer):
+ super(CsvBenchmarkResults, self).__init__()
+ self._results_writer = results_writer
+ self._did_write_header = False
+
+ def DidMeasurePage(self):
+ super(CsvBenchmarkResults, self).DidMeasurePage()
+
+ if not self._did_write_header:
+ self._did_write_header = True
+ row = ['url']
+ for name in self.field_names:
+ row.append('%s (%s)' % (name, self.field_units[name]))
+ self._results_writer.writerow(row)
+
+ row = [self._page.url]
+ for name in self.field_names:
+ value = self._page_values[name]
+ if self.field_types[name] == 'histogram':
+ avg, _ = GeomMeanAndStdDevFromHistogram(value)
+ row.append(avg)
+ elif isinstance(value, list):
+ row.append(_Mean(value))
+ else:
+ row.append(value)
+ self._results_writer.writerow(row)
+
+
+# TODO(nduca): Rename to page_benchmark
+class MultiPageBenchmark(page_test.PageTest):
+ """Glue code for running a benchmark across a set of pages.
+
+ To use this, subclass from the benchmark and override MeasurePage. For
+ example:
+
+ class BodyChildElementBenchmark(MultiPageBenchmark):
+ def MeasurePage(self, page, tab, results):
+ body_child_count = tab.runtime.Evaluate(
+ 'document.body.children.length')
+ results.Add('body_children', 'count', body_child_count)
+
+ if __name__ == '__main__':
+ multi_page_benchmark.Main(BodyChildElementBenchmark())
+
+ All benchmarks should include a unit test!
+
+ TODO(nduca): Add explanation of how to write the unit test.
+
+ To add test-specific options:
+
+ class BodyChildElementBenchmark(MultiPageBenchmark):
+ def AddOptions(parser):
+ parser.add_option('--element', action='store', default='body')
+
+ def MeasurePage(self, page, tab, results):
+ body_child_count = tab.runtime.Evaluate(
+ 'document.querySelector('%s').children.length')
+ results.Add('children', 'count', child_count)
+ """
+ def __init__(self):
+ super(MultiPageBenchmark, self).__init__('_RunTest')
+
+ def _RunTest(self, page, tab, results):
+ results.WillMeasurePage(page)
+ self.MeasurePage(page, tab, results)
+ results.DidMeasurePage()
+
+ def MeasurePage(self, page, tab, results):
+ """Override to actually measure the page's performance.
+
+ page is a page_set.Page
+ tab is an instance of telemetry.Tab
+
+ Should call results.Add(name, units, value) for each result, or raise an
+ exception on failure. The name and units of each Add() call must be
+ the same across all iterations. The name 'url' must not be used.
+
+ Prefer field names that are in accordance with python variable style. E.g.
+ field_name.
+
+ Put together:
+
+ def MeasurePage(self, page, tab, results):
+ res = tab.runtime.Evaluate('2+2')
+ if res != 4:
+ raise Exception('Oh, wow.')
+ results.Add('two_plus_two', 'count', res)
+ """
+ raise NotImplementedError()
diff --git a/tools/telemetry/telemetry/multi_page_benchmark_runner.py b/tools/telemetry/telemetry/multi_page_benchmark_runner.py
new file mode 100755
index 0000000..48837ee
--- /dev/null
+++ b/tools/telemetry/telemetry/multi_page_benchmark_runner.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import csv
+import inspect
+import logging
+import os
+import sys
+import traceback
+
+from telemetry import browser_finder
+from telemetry import browser_options
+from telemetry import multi_page_benchmark
+from telemetry import page_runner
+from telemetry import page_set
+
+
+def _Discover(start_dir, clazz):
+ """Discover all classes in |start_dir| which subclass |clazz|.
+
+ Args:
+ start_dir: The directory to recursively search.
+ clazz: The base class to search for.
+
+ Returns:
+ dict of {module_name: class}.
+ """
+ top_level_dir = os.path.join(start_dir, '..')
+ classes = {}
+ for dirpath, _, filenames in os.walk(start_dir):
+ for filename in filenames:
+ if not filename.endswith('.py'):
+ continue
+ name, _ = os.path.splitext(filename)
+ relpath = os.path.relpath(dirpath, top_level_dir)
+ fqn = relpath.replace('/', '.') + '.' + name
+ try:
+ module = __import__(fqn, fromlist=[True])
+ except Exception:
+ logging.error('While importing [%s]\n' % fqn)
+ traceback.print_exc()
+ continue
+ for name, obj in inspect.getmembers(module):
+ if inspect.isclass(obj):
+ if clazz in inspect.getmro(obj):
+ name = module.__name__.split('.')[-1]
+ classes[name] = obj
+ return classes
+
+
+def Main(benchmark_dir):
+ """Turns a MultiPageBenchmark into a command-line program.
+
+ Args:
+ benchmark_dir: Path to directory containing MultiPageBenchmarks.
+ """
+ benchmarks = _Discover(benchmark_dir, multi_page_benchmark.MultiPageBenchmark)
+
+ # Naively find the benchmark. If we use the browser options parser, we run
+ # the risk of failing to parse if we use a benchmark-specific parameter.
+ benchmark_name = None
+ for arg in sys.argv:
+ if arg in benchmarks:
+ benchmark_name = arg
+
+ options = browser_options.BrowserOptions()
+ parser = options.CreateParser('%prog [options] <benchmark> <page_set>')
+
+ benchmark = None
+ if benchmark_name is not None:
+ benchmark = benchmarks[benchmark_name]()
+ benchmark.AddOptions(parser)
+
+ _, args = parser.parse_args()
+
+ if benchmark is None or len(args) != 2:
+ parser.print_usage()
+ import page_sets # pylint: disable=F0401
+ print >> sys.stderr, 'Available benchmarks:\n%s\n' % ',\n'.join(
+ sorted(benchmarks.keys()))
+ print >> sys.stderr, 'Available page_sets:\n%s\n' % ',\n'.join(
+ sorted([os.path.relpath(f)
+ for f in page_sets.GetAllPageSetFilenames()]))
+ sys.exit(1)
+
+ ps = page_set.PageSet.FromFile(args[1])
+
+ benchmark.CustomizeBrowserOptions(options)
+ possible_browser = browser_finder.FindBrowser(options)
+ if not possible_browser:
+ print >> sys.stderr, """No browser found.\n
+Use --browser=list to figure out which are available.\n"""
+ sys.exit(1)
+
+ results = multi_page_benchmark.CsvBenchmarkResults(csv.writer(sys.stdout))
+ with page_runner.PageRunner(ps) as runner:
+ runner.Run(options, possible_browser, benchmark, results)
+ # When using an exact executable, assume it is a reference build for the
+ # purpose of outputting the perf results.
+ results.PrintSummary(options.browser_executable and '_ref' or '')
+
+ if len(results.page_failures):
+ logging.warning('Failed pages: %s', '\n'.join(
+ [failure['page'].url for failure in results.page_failures]))
+ return min(255, len(results.page_failures))
diff --git a/tools/telemetry/telemetry/multi_page_benchmark_unittest.py b/tools/telemetry/telemetry/multi_page_benchmark_unittest.py
new file mode 100644
index 0000000..ca59014
--- /dev/null
+++ b/tools/telemetry/telemetry/multi_page_benchmark_unittest.py
@@ -0,0 +1,92 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+
+from telemetry import multi_page_benchmark
+from telemetry import multi_page_benchmark_unittest_base
+from telemetry import page as page_module
+from telemetry import page_set
+from telemetry import wpr_modes
+
+class BenchThatFails(multi_page_benchmark.MultiPageBenchmark):
+ def MeasurePage(self, page, tab, results):
+ raise multi_page_benchmark.MeasurementFailure('Intentional failure.')
+
+class BenchThatHasDefaults(multi_page_benchmark.MultiPageBenchmark):
+ def AddOptions(self, parser):
+ parser.add_option('-x', dest='x', default=3)
+
+ def MeasurePage(self, page, tab, results):
+ assert self.options.x == 3
+ results.Add('x', 'ms', 7)
+
+class BenchForBlank(multi_page_benchmark.MultiPageBenchmark):
+ def MeasurePage(self, page, tab, results):
+ contents = tab.runtime.Evaluate('document.body.textContent')
+ assert contents.strip() == 'Hello world'
+
+class BenchForReplay(multi_page_benchmark.MultiPageBenchmark):
+ def MeasurePage(self, page, tab, results):
+ # Web Page Replay returns '404 Not found' if a page is not in the archive.
+ contents = tab.runtime.Evaluate('document.body.textContent')
+ if '404 Not Found' in contents.strip():
+ raise multi_page_benchmark.MeasurementFailure('Page not in archive.')
+
+class MultiPageBenchmarkUnitTest(
+ multi_page_benchmark_unittest_base.MultiPageBenchmarkUnitTestBase):
+
+ _wpr_mode = wpr_modes.WPR_OFF
+
+ def CustomizeOptionsForTest(self, options):
+ options.wpr_mode = self._wpr_mode
+
+ def testGotToBlank(self):
+ ps = self.CreatePageSetFromFileInUnittestDataDir('blank.html')
+ benchmark = BenchForBlank()
+ all_results = self.RunBenchmark(benchmark, ps)
+ self.assertEquals(0, len(all_results.page_failures))
+
+ def testFailure(self):
+ ps = self.CreatePageSetFromFileInUnittestDataDir('blank.html')
+ benchmark = BenchThatFails()
+ all_results = self.RunBenchmark(benchmark, ps)
+ self.assertEquals(1, len(all_results.page_failures))
+
+ def testDefaults(self):
+ ps = self.CreatePageSetFromFileInUnittestDataDir('blank.html')
+ benchmark = BenchThatHasDefaults()
+ all_results = self.RunBenchmark(benchmark, ps)
+ self.assertEquals(len(all_results.page_results), 1)
+ self.assertEquals(all_results.page_results[0]['x'], 7)
+
+ def testRecordAndReplay(self):
+ test_archive = '/tmp/google.wpr'
+ try:
+ ps = page_set.PageSet()
+ ps.archive_path = test_archive
+ benchmark = BenchForReplay()
+
+ # First record an archive with only www.google.com.
+ self._wpr_mode = wpr_modes.WPR_RECORD
+
+ ps.pages = [page_module.Page('http://www.google.com/')]
+ all_results = self.RunBenchmark(benchmark, ps)
+ self.assertEquals(0, len(all_results.page_failures))
+
+ # Now replay it and verify that google.com is found but foo.com is not.
+ self._wpr_mode = wpr_modes.WPR_REPLAY
+
+ ps.pages = [page_module.Page('http://www.foo.com/')]
+ all_results = self.RunBenchmark(benchmark, ps)
+ self.assertEquals(1, len(all_results.page_failures))
+
+ ps.pages = [page_module.Page('http://www.google.com/')]
+ all_results = self.RunBenchmark(benchmark, ps)
+ self.assertEquals(0, len(all_results.page_failures))
+
+ self.assertTrue(os.path.isfile(test_archive))
+
+ finally:
+ if os.path.isfile(test_archive):
+ os.remove(test_archive)
diff --git a/tools/telemetry/telemetry/multi_page_benchmark_unittest_base.py b/tools/telemetry/telemetry/multi_page_benchmark_unittest_base.py
new file mode 100644
index 0000000..01ec171
--- /dev/null
+++ b/tools/telemetry/telemetry/multi_page_benchmark_unittest_base.py
@@ -0,0 +1,49 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import unittest
+
+from telemetry import browser_finder
+from telemetry import multi_page_benchmark
+from telemetry import options_for_unittests
+from telemetry import page_runner
+from telemetry import page as page_module
+from telemetry import page_set
+
+class MultiPageBenchmarkUnitTestBase(unittest.TestCase):
+ """unittest.TestCase-derived class to help in the construction of unit tests
+ for a benchmark."""
+
+ def CreatePageSetFromFileInUnittestDataDir(self, test_filename):
+ base_dir = os.path.dirname(__file__)
+ page = page_module.Page(os.path.join('..', 'unittest_data', test_filename),
+ base_dir=base_dir)
+ ps = page_set.PageSet(base_dir=base_dir)
+ ps.pages.append(page)
+ return ps
+
+ def CustomizeOptionsForTest(self, options):
+ """Override to customize default options."""
+ pass
+
+ def RunBenchmark(self, benchmark, ps):
+ """Runs a benchmark against a pageset, returning the rows its outputs."""
+ options = options_for_unittests.Get()
+ assert options
+ temp_parser = options.CreateParser()
+ benchmark.AddOptions(temp_parser)
+ defaults = temp_parser.get_default_values()
+ for k, v in defaults.__dict__.items():
+ if hasattr(options, k):
+ continue
+ setattr(options, k, v)
+
+ benchmark.CustomizeBrowserOptions(options)
+ self.CustomizeOptionsForTest(options)
+ possible_browser = browser_finder.FindBrowser(options)
+
+ results = multi_page_benchmark.BenchmarkResults()
+ with page_runner.PageRunner(ps) as runner:
+ runner.Run(options, possible_browser, benchmark, results)
+ return results
diff --git a/tools/telemetry/telemetry/options_for_unittests.py b/tools/telemetry/telemetry/options_for_unittests.py
new file mode 100644
index 0000000..ca30208
--- /dev/null
+++ b/tools/telemetry/telemetry/options_for_unittests.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This module provides the global variable options_for_unittests.
+
+This is set to a BrowserOptions object by the test harness, or None
+if unit tests are not running.
+
+This allows multiple unit tests to use a specific
+browser, in face of multiple options."""
+_options = None
+_browser_type = None
+def Set(options, browser_type):
+ global _options
+ global _browser_type
+
+ _options = options
+ _browser_type = browser_type
+
+def Get():
+ if not _options:
+ return None
+
+ return _options.Copy()
+
+def GetBrowserType():
+ return _browser_type
diff --git a/tools/telemetry/telemetry/page.py b/tools/telemetry/telemetry/page.py
new file mode 100644
index 0000000..122e62b
--- /dev/null
+++ b/tools/telemetry/telemetry/page.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import urlparse
+
+class Page(object):
+ def __init__(self, url, attributes=None, base_dir=None):
+ parsed_url = urlparse.urlparse(url)
+ if not parsed_url.scheme:
+ abspath = os.path.abspath(os.path.join(base_dir, parsed_url.path))
+ if os.path.exists(abspath):
+ url = 'file://%s' % os.path.abspath(os.path.join(base_dir, url))
+ else:
+ raise Exception('URLs must be fully qualified: %s' % url)
+ self.url = url
+ self.interactions = 'scroll'
+ self.credentials = None
+ self.wait_time_after_navigate = 2
+ self.scroll_is_infinite = False
+ self.wait_for_javascript_expression = None
+
+ if attributes:
+ for k, v in attributes.iteritems():
+ setattr(self, k, v)
+
+ def __str__(self):
+ return self.url
diff --git a/tools/telemetry/telemetry/page_runner.py b/tools/telemetry/telemetry/page_runner.py
new file mode 100644
index 0000000..2fd7ee3
--- /dev/null
+++ b/tools/telemetry/telemetry/page_runner.py
@@ -0,0 +1,184 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import logging
+import os
+import time
+import traceback
+import urlparse
+import random
+import csv
+
+from telemetry import page_test
+from telemetry import util
+from telemetry import wpr_modes
+
+class PageState(object):
+ def __init__(self):
+ self.did_login = False
+
+class PageRunner(object):
+ """Runs a given test against a given test."""
+ def __init__(self, page_set):
+ self.page_set = page_set
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.Close()
+
+ def _ReorderPageSet(self, test_shuffle_order_file):
+ page_set_dict = {}
+ for page in self.page_set:
+ page_set_dict[page.url] = page
+
+ self.page_set.pages = []
+ with open(test_shuffle_order_file, 'rb') as csv_file:
+ csv_reader = csv.reader(csv_file)
+ csv_header = csv_reader.next()
+
+ if 'url' not in csv_header:
+ raise Exception('Unusable test_shuffle_order_file.')
+
+ url_index = csv_header.index('url')
+
+ for csv_row in csv_reader:
+ if csv_row[url_index] in page_set_dict:
+ self.page_set.pages.append(page_set_dict[csv_row[url_index]])
+ else:
+ raise Exception('Unusable test_shuffle_order_file.')
+
+ def Run(self, options, possible_browser, test, results):
+ archive_path = os.path.abspath(os.path.join(self.page_set.base_dir,
+ self.page_set.archive_path))
+ if options.wpr_mode == wpr_modes.WPR_OFF:
+ if os.path.isfile(archive_path):
+ possible_browser.options.wpr_mode = wpr_modes.WPR_REPLAY
+ else:
+ possible_browser.options.wpr_mode = wpr_modes.WPR_OFF
+ if not self.page_set.ContainsOnlyFileURLs():
+ logging.warning("""
+The page set archive %s does not exist, benchmarking against live sites!
+Results won't be repeatable or comparable.
+
+To fix this, either add svn-internal to your .gclient using
+http://goto/read-src-internal, or create a new archive using --record.
+""", os.path.relpath(archive_path))
+
+ credentials_path = None
+ if self.page_set.credentials_path:
+ credentials_path = os.path.join(self.page_set.base_dir,
+ self.page_set.credentials_path)
+ if not os.path.exists(credentials_path):
+ credentials_path = None
+
+ with possible_browser.Create() as b:
+ b.credentials.credentials_path = credentials_path
+ test.SetUpBrowser(b)
+
+ b.credentials.WarnIfMissingCredentials(self.page_set)
+
+ if not options.test_shuffle and options.test_shuffle_order_file is not\
+ None:
+ raise Exception('--test-shuffle-order-file requires --test-shuffle.')
+
+ # Set up a random generator for shuffling the page running order.
+ test_random = random.Random()
+
+ b.SetReplayArchivePath(archive_path)
+ with b.ConnectToNthTab(0) as tab:
+ if options.test_shuffle_order_file is None:
+ for _ in range(int(options.pageset_repeat)):
+ if options.test_shuffle:
+ test_random.shuffle(self.page_set)
+ for page in self.page_set:
+ for _ in range(int(options.page_repeat)):
+ self._RunPage(options, page, tab, test, results)
+ else:
+ self._ReorderPageSet(options.test_shuffle_order_file)
+ for page in self.page_set:
+ self._RunPage(options, page, tab, test, results)
+
+ def _RunPage(self, options, page, tab, test, results):
+ logging.info('Running %s' % page.url)
+
+ page_state = PageState()
+ try:
+ did_prepare = self.PreparePage(page, tab, page_state, results)
+ except Exception, ex:
+ logging.error('Unexpected failure while running %s: %s',
+ page.url, traceback.format_exc())
+ self.CleanUpPage(page, tab, page_state)
+ raise
+
+ if not did_prepare:
+ self.CleanUpPage(page, tab, page_state)
+ return
+
+ try:
+ test.Run(options, page, tab, results)
+ except page_test.Failure, ex:
+ logging.info('%s: %s', ex, page.url)
+ results.AddFailure(page, ex, traceback.format_exc())
+ return
+ except util.TimeoutException, ex:
+ logging.warning('Timed out while running %s', page.url)
+ results.AddFailure(page, ex, traceback.format_exc())
+ return
+ except Exception, ex:
+ logging.error('Unexpected failure while running %s: %s',
+ page.url, traceback.format_exc())
+ raise
+ finally:
+ self.CleanUpPage(page, tab, page_state)
+
+ def Close(self):
+ pass
+
+ @staticmethod
+ def WaitForPageToLoad(expression, tab):
+ def IsPageLoaded():
+ return tab.runtime.Evaluate(expression)
+
+ # Wait until the form is submitted and the page completes loading.
+ util.WaitFor(lambda: IsPageLoaded(), 60) # pylint: disable=W0108
+
+ def PreparePage(self, page, tab, page_state, results):
+ parsed_url = urlparse.urlparse(page.url)
+ if parsed_url[0] == 'file':
+ path = os.path.join(self.page_set.base_dir,
+ parsed_url.netloc,
+ parsed_url.path) # pylint: disable=E1101
+ dirname, filename = os.path.split(path)
+ tab.browser.SetHTTPServerDirectory(dirname)
+ target_side_url = tab.browser.http_server.UrlOf(filename)
+ else:
+ target_side_url = page.url
+
+ if page.credentials:
+ page_state.did_login = tab.browser.credentials.LoginNeeded(
+ tab, page.credentials)
+ if not page_state.did_login:
+ msg = 'Could not login to %s on %s' % (page.credentials,
+ target_side_url)
+ logging.info(msg)
+ results.AddFailure(page, msg, "")
+ return False
+
+ tab.page.Navigate(target_side_url)
+
+ # Wait for unpredictable redirects.
+ if page.wait_time_after_navigate:
+ time.sleep(page.wait_time_after_navigate)
+ if page.wait_for_javascript_expression is not None:
+ self.WaitForPageToLoad(page.wait_for_javascript_expression, tab)
+
+ tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
+ return True
+
+ def CleanUpPage(self, page, tab, page_state): # pylint: disable=R0201
+ if page.credentials and page_state.did_login:
+ tab.browser.credentials.LoginNoLongerNeeded(tab, page.credentials)
+ tab.runtime.Evaluate("""window.chrome && chrome.benchmarking &&
+ chrome.benchmarking.closeConnections()""")
diff --git a/tools/telemetry/telemetry/page_runner_unittest.py b/tools/telemetry/telemetry/page_runner_unittest.py
new file mode 100644
index 0000000..44fdce0
--- /dev/null
+++ b/tools/telemetry/telemetry/page_runner_unittest.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import tempfile
+import unittest
+
+from telemetry import browser_finder
+from telemetry import page as page_module
+from telemetry import page_set
+from telemetry import page_test
+from telemetry import page_runner
+from telemetry import options_for_unittests
+
+SIMPLE_CREDENTIALS_STRING = """
+{
+ "test": {
+ "username": "example",
+ "password": "asdf"
+ }
+}
+"""
+class StubCredentialsBackend(object):
+ def __init__(self, login_return_value):
+ self.did_get_login = False
+ self.did_get_login_no_longer_needed = False
+ self.login_return_value = login_return_value
+
+ @property
+ def credentials_type(self): # pylint: disable=R0201
+ return 'test'
+
+ def LoginNeeded(self, tab, config): # pylint: disable=W0613
+ self.did_get_login = True
+ return self.login_return_value
+
+ def LoginNoLongerNeeded(self, tab): # pylint: disable=W0613
+ self.did_get_login_no_longer_needed = True
+
+class PageRunnerTests(unittest.TestCase):
+ # TODO(nduca): Move the basic "test failed, test succeeded" tests from
+ # multi_page_benchmark_unittest to here.
+
+ def testCredentialsWhenLoginFails(self):
+ results = page_test.PageTestResults()
+ credentials_backend = StubCredentialsBackend(login_return_value=False)
+ did_run = self.runCredentialsTest(credentials_backend, results)
+ assert credentials_backend.did_get_login == True
+ assert credentials_backend.did_get_login_no_longer_needed == False
+ assert did_run == False
+
+ def testCredentialsWhenLoginSucceeds(self):
+ results = page_test.PageTestResults()
+ credentials_backend = StubCredentialsBackend(login_return_value=True)
+ did_run = self.runCredentialsTest(credentials_backend, results)
+ assert credentials_backend.did_get_login == True
+ assert credentials_backend.did_get_login_no_longer_needed == True
+ assert did_run
+
+ def runCredentialsTest(self, # pylint: disable=R0201
+ credentials_backend,
+ results):
+ page = page_module.Page('http://www.google.com')
+ page.credentials = "test"
+ ps = page_set.PageSet()
+ ps.pages.append(page)
+
+ did_run = [False]
+
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(SIMPLE_CREDENTIALS_STRING)
+ f.flush()
+ ps.credentials_path = f.name
+
+ class TestThatInstallsCredentialsBackend(page_test.PageTest):
+ def __init__(self, credentials_backend):
+ super(TestThatInstallsCredentialsBackend, self).__init__('RunTest')
+ self._credentials_backend = credentials_backend
+
+ def SetUpBrowser(self, browser):
+ browser.credentials.AddBackend(self._credentials_backend)
+
+ def RunTest(self, page, tab, results): # pylint: disable=W0613,R0201
+ did_run[0] = True
+
+ test = TestThatInstallsCredentialsBackend(credentials_backend)
+ with page_runner.PageRunner(ps) as runner:
+ options = options_for_unittests.Get()
+ possible_browser = browser_finder.FindBrowser(options)
+ runner.Run(options, possible_browser, test, results)
+
+ return did_run[0]
diff --git a/tools/telemetry/telemetry/page_set.py b/tools/telemetry/telemetry/page_set.py
new file mode 100644
index 0000000..e41cdb3
--- /dev/null
+++ b/tools/telemetry/telemetry/page_set.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import json
+import os
+import urlparse
+
+from telemetry import page as page_module
+
+class PageSet(object):
+ def __init__(self, base_dir='', attributes=None):
+ self.description = ''
+ self.archive_path = ''
+ self.base_dir = base_dir
+ self.credentials_path = None
+
+ if attributes:
+ for k, v in attributes.iteritems():
+ setattr(self, k, v)
+
+ self.pages = []
+
+ @classmethod
+ def FromFile(cls, file_path):
+ with open(file_path, 'r') as f:
+ contents = f.read()
+ data = json.loads(contents)
+ return cls.FromDict(data, os.path.dirname(file_path))
+
+ @classmethod
+ def FromDict(cls, data, file_path=''):
+ page_set = cls(file_path, data)
+ for page_attributes in data['pages']:
+ url = page_attributes.pop('url')
+ page = page_module.Page(url, attributes=page_attributes,
+ base_dir=file_path)
+ page_set.pages.append(page)
+ return page_set
+
+ def ContainsOnlyFileURLs(self):
+ for page in self.pages:
+ parsed_url = urlparse.urlparse(page.url)
+ if parsed_url.scheme != 'file':
+ return False
+ return True
+
+ def __iter__(self):
+ return self.pages.__iter__()
+
+ def __len__(self):
+ return len(self.pages)
+
+ def __getitem__(self, key):
+ return self.pages[key]
+
+ def __setitem__(self, key, value):
+ self.pages[key] = value
diff --git a/tools/telemetry/telemetry/page_set_unittest.py b/tools/telemetry/telemetry/page_set_unittest.py
new file mode 100644
index 0000000..ef1a83c
--- /dev/null
+++ b/tools/telemetry/telemetry/page_set_unittest.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import tempfile
+import unittest
+
+from telemetry import page_set
+
+simple_set = """
+{"description": "hello",
+ "archive_path": "foo.wpr",
+ "pages": [
+ {"url": "http://www.foo.com/"}
+ ]
+}
+"""
+
+class TestPageSet(unittest.TestCase):
+ def testSimpleSet(self):
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(simple_set)
+ f.flush()
+ ps = page_set.PageSet.FromFile(f.name)
+
+ self.assertEquals('hello', ps.description)
+ self.assertEquals('foo.wpr', ps.archive_path)
+ self.assertEquals(1, len(ps.pages))
+ self.assertEquals('http://www.foo.com/', ps.pages[0].url)
diff --git a/tools/telemetry/telemetry/page_test.py b/tools/telemetry/telemetry/page_test.py
new file mode 100644
index 0000000..2d4d24f
--- /dev/null
+++ b/tools/telemetry/telemetry/page_test.py
@@ -0,0 +1,50 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+class Failure(Exception):
+ """Exception that can be thrown from MultiPageBenchmark to indicate an
+ undesired but designed-for problem."""
+ pass
+
+class PageTestResults(object):
+ def __init__(self):
+ self.page_failures = []
+
+ def AddFailure(self, page, message, details):
+ self.page_failures.append({'page': page,
+ 'message': message,
+ 'details': details})
+
+class PageTest(object):
+ """A class styled on unittest.TestCase for creating page-specific tests."""
+
+ def __init__(self, test_method_name):
+ self.options = None
+ try:
+ self._test_method = getattr(self, test_method_name)
+ except AttributeError:
+ raise ValueError, 'No such method %s.%s' % (
+ self.__class_, test_method_name) # pylint: disable=E1101
+
+ def AddOptions(self, parser):
+ """Override to expose command-line options for this benchmark.
+
+ The provided parser is an optparse.OptionParser instance and accepts all
+ normal results. The parsed options are available in MeasurePage as
+ self.options."""
+ pass
+
+ def CustomizeBrowserOptions(self, options):
+ """Override to add test-specific options to the BrowserOptions object"""
+ pass
+
+ def SetUpBrowser(self, browser):
+ """Override to customize the browser right after it has launched."""
+ pass
+
+ def Run(self, options, page, tab, results):
+ self.options = options
+ try:
+ self._test_method(page, tab, results)
+ finally:
+ self.options = None
diff --git a/tools/telemetry/telemetry/platform.py b/tools/telemetry/telemetry/platform.py
new file mode 100644
index 0000000..c668f7b
--- /dev/null
+++ b/tools/telemetry/telemetry/platform.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+class Platform(object):
+ """The platform that the target browser is running on.
+
+ Provides a limited interface to obtain stats from the platform itself, where
+ possible.
+ """
+
+ def GetSurfaceCollector(self, trace_tag):
+ """Platforms may be able to collect GL surface stats."""
+ class StubSurfaceCollector(object):
+ def __init__(self, trace_tag):
+ pass
+ def __enter__(self):
+ pass
+ def __exit__(self, *args):
+ pass
+
+ return StubSurfaceCollector(trace_tag)
diff --git a/tools/telemetry/telemetry/possible_browser.py b/tools/telemetry/telemetry/possible_browser.py
new file mode 100644
index 0000000..2cb995f
--- /dev/null
+++ b/tools/telemetry/telemetry/possible_browser.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+class PossibleBrowser(object):
+ """A browser that can be controlled.
+
+ Call Create() to launch the browser and begin manipulating it..
+ """
+
+ def __init__(self, browser_type, options):
+ self.browser_type = browser_type
+ self._options = options
+
+ def __repr__(self):
+ return 'PossibleBrowser(browser_type=%s)' % self.browser_type
+
+ @property
+ def options(self):
+ return self._options
+
+ def Create(self):
+ raise NotImplementedError()
diff --git a/tools/telemetry/telemetry/run_tests.py b/tools/telemetry/telemetry/run_tests.py
new file mode 100644
index 0000000..c1f4eb7
--- /dev/null
+++ b/tools/telemetry/telemetry/run_tests.py
@@ -0,0 +1,143 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import fnmatch
+import logging
+import os
+import traceback
+import unittest
+
+from telemetry import browser_options
+from telemetry import options_for_unittests
+
+def RequiresBrowserOfType(*types):
+ def wrap(func):
+ func._requires_browser_types = types
+ return func
+ return wrap
+
+def Discover(start_dir, pattern = 'test*.py', top_level_dir = None):
+ if hasattr(unittest.defaultTestLoader, 'discover'):
+ return unittest.defaultTestLoader.discover( # pylint: disable=E1101
+ start_dir,
+ pattern,
+ top_level_dir)
+
+ modules = []
+ for dirpath, _, filenames in os.walk(start_dir):
+ for filename in filenames:
+ if not filename.endswith('.py'):
+ continue
+
+ if not fnmatch.fnmatch(filename, pattern):
+ continue
+
+ if filename.startswith('.') or filename.startswith('_'):
+ continue
+ name, _ = os.path.splitext(filename)
+
+ relpath = os.path.relpath(dirpath, top_level_dir)
+ fqn = relpath.replace('/', '.') + '.' + name
+
+ # load the module
+ try:
+ module = __import__(fqn, fromlist=[True])
+ except Exception:
+ print 'While importing [%s]\n' % fqn
+ traceback.print_exc()
+ continue
+ modules.append(module)
+
+ loader = unittest.defaultTestLoader
+ subsuites = []
+ for module in modules:
+ if hasattr(module, 'suite'):
+ new_suite = module.suite()
+ else:
+ new_suite = loader.loadTestsFromModule(module)
+ if new_suite.countTestCases():
+ subsuites.append(new_suite)
+ return unittest.TestSuite(subsuites)
+
+def FilterSuite(suite, predicate):
+ new_suite = unittest.TestSuite()
+ for x in suite:
+ if isinstance(x, unittest.TestSuite):
+ subsuite = FilterSuite(x, predicate)
+ if subsuite.countTestCases() == 0:
+ continue
+
+ new_suite.addTest(subsuite)
+ continue
+
+ assert isinstance(x, unittest.TestCase)
+ if predicate(x):
+ new_suite.addTest(x)
+
+ return new_suite
+
+def DiscoverAndRunTests(dir_name, args, top_level_dir):
+ suite = Discover(dir_name, '*_unittest.py', top_level_dir)
+
+ def IsTestSelected(test):
+ if len(args) != 0:
+ found = False
+ for name in args:
+ if name in test.id():
+ found = True
+ if not found:
+ return False
+
+ if hasattr(test, '_testMethodName'):
+ method = getattr(test, test._testMethodName) # pylint: disable=W0212
+ if hasattr(method, '_requires_browser_types'):
+ types = method._requires_browser_types # pylint: disable=W0212
+ if options_for_unittests.GetBrowserType() not in types:
+ logging.debug('Skipping test %s because it requires %s' %
+ (test.id(), types))
+ return False
+
+ return True
+
+ filtered_suite = FilterSuite(suite, IsTestSelected)
+ runner = unittest.TextTestRunner(verbosity = 2)
+ test_result = runner.run(filtered_suite)
+ return len(test_result.errors) + len(test_result.failures)
+
+def Main(args, start_dir, top_level_dir):
+ """Unit test suite that collects all test cases for telemetry."""
+ default_options = browser_options.BrowserOptions()
+ default_options.browser_type = 'any'
+
+ parser = default_options.CreateParser('run_tests [options] [test names]')
+ parser.add_option('--repeat-count', dest='run_test_repeat_count',
+ type='int', default=1,
+ help='Repeats each a provided number of times.')
+
+ _, args = parser.parse_args(args)
+
+ if default_options.verbosity == 0:
+ logging.getLogger().setLevel(logging.ERROR)
+
+ from telemetry import browser_finder
+ browser_to_create = browser_finder.FindBrowser(default_options)
+ if browser_to_create == None:
+ logging.error('No browser found of type %s. Cannot run tests.',
+ default_options.browser_type)
+ logging.error('Re-run with --browser=list to see available browser types.')
+ return 1
+
+ options_for_unittests.Set(default_options,
+ browser_to_create.browser_type)
+ olddir = os.getcwd()
+ num_errors = 0
+ try:
+ os.chdir(top_level_dir)
+ for _ in range(
+ default_options.run_test_repeat_count): # pylint: disable=E1101
+ num_errors += DiscoverAndRunTests(start_dir, args, top_level_dir)
+ finally:
+ os.chdir(olddir)
+ options_for_unittests.Set(None, None)
+
+ return max(num_errors, 255)
diff --git a/tools/telemetry/telemetry/simple_mock.py b/tools/telemetry/telemetry/simple_mock.py
new file mode 100644
index 0000000..6f7afb6
--- /dev/null
+++ b/tools/telemetry/telemetry/simple_mock.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""A very very simple mock object harness."""
+
+DONT_CARE = ''
+
+class MockFunctionCall(object):
+ def __init__(self, name):
+ self.name = name
+ self.args = tuple()
+ self.return_value = None
+ self.when_called_handlers = []
+
+ def WithArgs(self, *args):
+ self.args = args
+ return self
+
+ def WillReturn(self, value):
+ self.return_value = value
+ return self
+
+ def WhenCalled(self, handler):
+ self.when_called_handlers.append(handler)
+
+ def VerifyEquals(self, got):
+ if self.name != got.name:
+ raise Exception('Self %s, got %s' % (repr(self), repr(got)))
+ if len(self.args) != len(got.args):
+ raise Exception('Self %s, got %s' % (repr(self), repr(got)))
+ for i in range(len(self.args)):
+ self_a = self.args[i]
+ got_a = got.args[i]
+ if self_a == DONT_CARE:
+ continue
+ if self_a != got_a:
+ raise Exception('Self %s, got %s' % (repr(self), repr(got)))
+
+ def __repr__(self):
+ def arg_to_text(a):
+ if a == DONT_CARE:
+ return '_'
+ return repr(a)
+ args_text = ', '.join([arg_to_text(a) for a in self.args])
+ if self.return_value in (None, DONT_CARE):
+ return '%s(%s)' % (self.name, args_text)
+ return '%s(%s)->%s' % (self.name, args_text, repr(self.return_value))
+
+class MockTrace(object):
+ def __init__(self):
+ self.expected_calls = []
+ self.next_call_index = 0
+
+class MockObject(object):
+ def __init__(self, parent_mock = None):
+ if parent_mock:
+ self._trace = parent_mock._trace # pylint: disable=W0212
+ else:
+ self._trace = MockTrace()
+
+ def __setattr__(self, name, value):
+ if (not hasattr(self, '_trace') or
+ hasattr(value, 'is_hook')):
+ object.__setattr__(self, name, value)
+ return
+ assert isinstance(value, MockObject)
+ object.__setattr__(self, name, value)
+
+ def ExpectCall(self, func_name, *args):
+ assert self._trace.next_call_index == 0
+ if not hasattr(self, func_name):
+ self._install_hook(func_name)
+
+ call = MockFunctionCall(func_name)
+ self._trace.expected_calls.append(call)
+ call.WithArgs(*args)
+ return call
+
+ def _install_hook(self, func_name):
+ def handler(*args):
+ got_call = MockFunctionCall(
+ func_name).WithArgs(*args).WillReturn(DONT_CARE)
+ if self._trace.next_call_index >= len(self._trace.expected_calls):
+ raise Exception(
+ 'Call to %s was not expected, at end of programmed trace.' %
+ repr(got_call))
+ expected_call = self._trace.expected_calls[
+ self._trace.next_call_index]
+ expected_call.VerifyEquals(got_call)
+ self._trace.next_call_index += 1
+ for h in expected_call.when_called_handlers:
+ h(*args)
+ return expected_call.return_value
+ handler.is_hook = True
+ setattr(self, func_name, handler)
+
+
+
diff --git a/tools/telemetry/telemetry/simple_mock_unittest.py b/tools/telemetry/telemetry/simple_mock_unittest.py
new file mode 100644
index 0000000..edbdbb2
--- /dev/null
+++ b/tools/telemetry/telemetry/simple_mock_unittest.py
@@ -0,0 +1,84 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+
+from telemetry import simple_mock
+
+_ = simple_mock.DONT_CARE
+
+class SimpleMockUnitTest(unittest.TestCase):
+ def testBasic(self):
+ mock = simple_mock.MockObject()
+ mock.ExpectCall('foo')
+
+ mock.foo()
+
+ def testReturn(self):
+ mock = simple_mock.MockObject()
+ mock.ExpectCall('foo').WillReturn(7)
+
+ ret = mock.foo()
+ self.assertEquals(ret, 7)
+
+ def testArgs(self):
+ mock = simple_mock.MockObject()
+ mock.ExpectCall('foo').WithArgs(3, 4)
+
+ mock.foo(3, 4)
+
+ def testArgs2(self):
+ mock = simple_mock.MockObject()
+ mock.ExpectCall('foo', 3, 4)
+
+ mock.foo(3, 4)
+
+ def testArgsMismatch(self):
+ mock = simple_mock.MockObject()
+ mock.ExpectCall('foo').WithArgs(3, 4)
+
+ self.assertRaises(Exception,
+ lambda: mock.foo(4, 4))
+
+
+ def testArgsDontCare(self):
+ mock = simple_mock.MockObject()
+ mock.ExpectCall('foo').WithArgs(_, 4)
+
+ mock.foo(4, 4)
+
+ def testOnCall(self):
+ mock = simple_mock.MockObject()
+
+ handler_called = []
+ def Handler(arg0):
+ assert arg0 == 7
+ handler_called.append(True)
+ mock.ExpectCall('baz', 7).WhenCalled(Handler)
+
+ mock.baz(7)
+ self.assertTrue(len(handler_called) > 0)
+
+
+ def testSubObject(self):
+ mock = simple_mock.MockObject()
+ mock.bar = simple_mock.MockObject(mock)
+
+ mock.ExpectCall('foo').WithArgs(_, 4)
+ mock.bar.ExpectCall('baz')
+
+ mock.foo(0, 4)
+ mock.bar.baz()
+
+ def testSubObjectMismatch(self):
+ mock = simple_mock.MockObject()
+ mock.bar = simple_mock.MockObject(mock)
+
+ mock.ExpectCall('foo').WithArgs(_, 4)
+ mock.bar.ExpectCall('baz')
+
+ self.assertRaises(
+ Exception,
+ lambda: mock.bar.baz()) # pylint: disable=W0108
+
+
diff --git a/tools/telemetry/telemetry/system_stub.py b/tools/telemetry/telemetry/system_stub.py
new file mode 100644
index 0000000..ce35e9d
--- /dev/null
+++ b/tools/telemetry/telemetry/system_stub.py
@@ -0,0 +1,140 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Provides stubs for os, sys and subprocess for testing
+
+This test allows one to test code that itself uses os, sys, and subprocess.
+"""
+
+import os
+import shlex
+import sys as real_sys
+
+class Override(object):
+ def __init__(self, base_module, module_list):
+ stubs = {'adb_commands': AdbCommandsModuleStub,
+ 'os': OsModuleStub,
+ 'subprocess': SubprocessModuleStub,
+ 'sys': SysModuleStub,
+ }
+ self.adb_commands = None
+ self.os = None
+ self.subprocess = None
+ self.sys = None
+
+ self._base_module = base_module
+ self._overrides = {}
+
+ for module_name in module_list:
+ self._overrides[module_name] = getattr(base_module, module_name)
+ setattr(self, module_name, stubs[module_name]())
+ setattr(base_module, module_name, getattr(self, module_name))
+
+ if self.os and self.sys:
+ self.os.path.sys = self.sys
+
+ def __del__(self):
+ assert not len(self._overrides)
+
+ def Restore(self):
+ for module_name, original_module in self._overrides.iteritems():
+ setattr(self._base_module, module_name, original_module)
+ self._overrides = {}
+
+class AdbCommandsModuleStub(object):
+# adb not even found
+# android_browser_finder not returning
+ class AdbCommandsStub(object):
+ def __init__(self, module, device):
+ self._module = module
+ self._device = device
+ self.is_root_enabled = True
+
+ def RunShellCommand(self, args):
+ if isinstance(args, basestring):
+ args = shlex.split(args)
+ handler = self._module.shell_command_handlers[args[0]]
+ return handler(args)
+
+ def IsRootEnabled(self):
+ return self.is_root_enabled
+
+ def __init__(self):
+ self.attached_devices = []
+ self.shell_command_handlers = {}
+
+ def AdbCommandsStubConstructor(device=None):
+ return AdbCommandsModuleStub.AdbCommandsStub(self, device)
+ self.AdbCommands = AdbCommandsStubConstructor
+
+ @staticmethod
+ def IsAndroidSupported():
+ return True
+
+ def GetAttachedDevices(self):
+ return self.attached_devices
+
+ @staticmethod
+ def HasForwarder(_):
+ return True
+
+class OsModuleStub(object):
+ class OsPathModuleStub(object):
+ def __init__(self, sys_module):
+ self.sys = sys_module
+ self.files = []
+
+ def exists(self, path):
+ return path in self.files
+
+ def join(self, *args):
+ if self.sys.platform.startswith('win'):
+ tmp = os.path.join(*args)
+ return tmp.replace('/', '\\')
+ else:
+ tmp = os.path.join(*args)
+ return tmp.replace('\\', '/')
+
+ def dirname(self, filename): # pylint: disable=R0201
+ return os.path.dirname(filename)
+
+ def __init__(self, sys_module=real_sys):
+ self.path = OsModuleStub.OsPathModuleStub(sys_module)
+ self.display = ':0'
+ self.local_app_data = None
+ self.program_files = None
+ self.program_files_x86 = None
+ self.devnull = os.devnull
+
+ def getenv(self, name):
+ if name == 'DISPLAY':
+ return self.display
+ elif name == 'LOCALAPPDATA':
+ return self.local_app_data
+ elif name == 'PROGRAMFILES':
+ return self.program_files
+ elif name == 'PROGRAMFILES(X86)':
+ return self.program_files_x86
+ raise Exception('Unsupported getenv')
+
+class SubprocessModuleStub(object):
+ class PopenStub(object):
+ def __init__(self):
+ self.communicate_result = ('', '')
+
+ def __call__(self, args, **kwargs):
+ return self
+
+ def communicate(self):
+ return self.communicate_result
+
+ def __init__(self):
+ self.Popen = SubprocessModuleStub.PopenStub()
+ self.PIPE = None
+
+ def call(self, *args, **kwargs):
+ raise NotImplementedError()
+
+class SysModuleStub(object):
+ def __init__(self):
+ self.platform = ''
diff --git a/tools/telemetry/telemetry/tab.py b/tools/telemetry/telemetry/tab.py
new file mode 100644
index 0000000..527a2e0
--- /dev/null
+++ b/tools/telemetry/telemetry/tab.py
@@ -0,0 +1,77 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+from telemetry import inspector_console
+from telemetry import inspector_page
+from telemetry import inspector_runtime
+from telemetry import util
+
+DEFAULT_TAB_TIMEOUT = 60
+
+class Tab(object):
+ """Represents a tab in the browser
+
+ The important parts of the Tab object are in the runtime and page objects.
+ E.g.:
+ # Navigates the tab to a given url.
+ tab.page.Navigate('http://www.google.com/')
+
+ # Evaluates 1+1 in the tab's javascript context.
+ tab.runtime.Evaluate('1+1')
+ """
+ def __init__(self, browser, inspector_backend):
+ self._browser = browser
+ self._inspector_backend = inspector_backend
+ self._page = inspector_page.InspectorPage(self._inspector_backend)
+ self._runtime = inspector_runtime.InspectorRuntime(self._inspector_backend)
+ self._console = inspector_console.InspectorConsole(self._inspector_backend)
+
+ def __del__(self):
+ self.Close()
+
+ def Close(self):
+ self._console = None
+ self._runtime = None
+ self._page = None
+ if self._inspector_backend:
+ self._inspector_backend.Close()
+ self._inspector_backend = None
+ self._browser = None
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.Close()
+
+ @property
+ def browser(self):
+ """The browser in which this tab resides."""
+ return self._browser
+
+ @property
+ def page(self):
+ """Methods for interacting with the current page."""
+ return self._page
+
+ @property
+ def runtime(self):
+ """Methods for interacting with the page's javascript runtime."""
+ return self._runtime
+
+ @property
+ def console(self):
+ """Methods for interacting with the page's console objec."""
+ return self._console
+
+ def WaitForDocumentReadyStateToBeComplete(self, timeout=DEFAULT_TAB_TIMEOUT):
+ util.WaitFor(
+ lambda: self._runtime.Evaluate('document.readyState') == 'complete',
+ timeout)
+
+ def WaitForDocumentReadyStateToBeInteractiveOrBetter(
+ self, timeout=DEFAULT_TAB_TIMEOUT):
+ def IsReadyStateInteractiveOrBetter():
+ rs = self._runtime.Evaluate('document.readyState')
+ return rs == 'complete' or rs == 'interactive'
+ util.WaitFor(IsReadyStateInteractiveOrBetter, timeout)
diff --git a/tools/telemetry/telemetry/tab_crash_exception.py b/tools/telemetry/telemetry/tab_crash_exception.py
new file mode 100644
index 0000000..434a883
--- /dev/null
+++ b/tools/telemetry/telemetry/tab_crash_exception.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+class TabCrashException(Exception):
+ """Represnets a crash of the current tab, but not the overall browser.
+
+ In this state, the tab is gone, but the underlying browser is still alive."""
+ pass
+
diff --git a/tools/telemetry/telemetry/tab_test_case.py b/tools/telemetry/telemetry/tab_test_case.py
new file mode 100644
index 0000000..cbd52d1
--- /dev/null
+++ b/tools/telemetry/telemetry/tab_test_case.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+
+from telemetry import browser_finder
+from telemetry import options_for_unittests
+
+class TabTestCase(unittest.TestCase):
+ def setUp(self):
+ self._browser = None
+ self._tab = None
+ options = options_for_unittests.Get()
+ browser_to_create = browser_finder.FindBrowser(options)
+ if not browser_to_create:
+ raise Exception('No browser found, cannot continue test.')
+ try:
+ self._browser = browser_to_create.Create()
+ self._tab = self._browser.ConnectToNthTab(0)
+ except:
+ self.tearDown()
+ raise
+
+ def tearDown(self):
+ if self._tab:
+ self._tab.Close()
+ if self._browser:
+ self._browser.Close()
+
diff --git a/tools/telemetry/telemetry/tab_unittest.py b/tools/telemetry/telemetry/tab_unittest.py
new file mode 100644
index 0000000..0d95900
--- /dev/null
+++ b/tools/telemetry/telemetry/tab_unittest.py
@@ -0,0 +1,24 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+from telemetry import tab_test_case
+from telemetry import tab_crash_exception
+
+class TabTest(tab_test_case.TabTestCase):
+ def testNavigateAndWaitToForCompleteState(self):
+ self._tab.page.Navigate('http://www.google.com')
+ self._tab.WaitForDocumentReadyStateToBeComplete()
+
+ def testNavigateAndWaitToForInteractiveState(self):
+ self._tab.page.Navigate('http://www.google.com')
+ self._tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
+
+ def testTabBrowserIsRightBrowser(self):
+ self.assertEquals(self._tab.browser, self._browser)
+
+ def testRendererCrash(self):
+ self.assertRaises(tab_crash_exception.TabCrashException,
+ lambda: self._tab.page.Navigate('chrome://crash',
+ timeout=5))
+
+
diff --git a/tools/telemetry/telemetry/temporary_http_server.py b/tools/telemetry/telemetry/temporary_http_server.py
new file mode 100644
index 0000000..952f942
--- /dev/null
+++ b/tools/telemetry/telemetry/temporary_http_server.py
@@ -0,0 +1,63 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import socket
+import subprocess
+import sys
+import urlparse
+
+class TemporaryHTTPServer(object):
+ def __init__(self, browser_backend, path):
+ self._server = None
+ self._devnull = None
+ self._path = path
+ self._forwarder = None
+
+ tmp = socket.socket()
+ tmp.bind(('', 0))
+ port = tmp.getsockname()[1]
+ tmp.close()
+ self._host_port = port
+
+ assert os.path.exists(path)
+ assert os.path.isdir(path)
+
+ self._devnull = open(os.devnull, 'w')
+ self._server = subprocess.Popen(
+ [sys.executable, '-m', 'SimpleHTTPServer', str(self._host_port)],
+ cwd=self._path,
+ stdout=self._devnull, stderr=self._devnull)
+
+ self._forwarder = browser_backend.CreateForwarder(self._host_port)
+
+ @property
+ def path(self):
+ return self._path
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.Close()
+
+ def __del__(self):
+ self.Close()
+
+ def Close(self):
+ if self._forwarder:
+ self._forwarder.Close()
+ self._forwarder = None
+ if self._server:
+ self._server.kill()
+ self._server = None
+ if self._devnull:
+ self._devnull.close()
+ self._devnull = None
+
+ @property
+ def url(self):
+ return self._forwarder.url
+
+ def UrlOf(self, path):
+ return urlparse.urljoin(self.url, path)
diff --git a/tools/telemetry/telemetry/temporary_http_server_unittest.py b/tools/telemetry/telemetry/temporary_http_server_unittest.py
new file mode 100644
index 0000000..f77d0e8d
--- /dev/null
+++ b/tools/telemetry/telemetry/temporary_http_server_unittest.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import unittest
+
+from telemetry import browser_finder
+from telemetry import options_for_unittests
+
+class TemporaryHTTPServerTest(unittest.TestCase):
+ def testBasicHosting(self):
+ unittest_data_dir = os.path.join(os.path.dirname(__file__),
+ '..', 'unittest_data')
+ options = options_for_unittests.Get()
+ browser_to_create = browser_finder.FindBrowser(options)
+ with browser_to_create.Create() as b:
+ b.SetHTTPServerDirectory(unittest_data_dir)
+ with b.ConnectToNthTab(0) as t:
+ t.page.Navigate(b.http_server.UrlOf('/blank.html'))
+ t.WaitForDocumentReadyStateToBeComplete()
+ x = t.runtime.Evaluate('document.body.innerHTML')
+ x = x.strip()
+
+ self.assertEquals(x, 'Hello world')
+
diff --git a/tools/telemetry/telemetry/util.py b/tools/telemetry/telemetry/util.py
new file mode 100644
index 0000000..f74e4db
--- /dev/null
+++ b/tools/telemetry/telemetry/util.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import inspect
+import time
+
+class TimeoutException(Exception):
+ pass
+
+def WaitFor(condition,
+ timeout, poll_interval=0.1,
+ pass_time_left_to_func=False):
+ assert isinstance(condition, type(lambda: None)) # is function
+ start_time = time.time()
+ while True:
+ if pass_time_left_to_func:
+ res = condition((start_time + timeout) - time.time())
+ else:
+ res = condition()
+ if res:
+ break
+ if time.time() - start_time > timeout:
+ if condition.__name__ == '<lambda>':
+ try:
+ condition_string = inspect.getsource(condition).strip()
+ except IOError:
+ condition_string = condition.__name__
+ else:
+ condition_string = condition.__name__
+ raise TimeoutException('Timed out while waiting %ds for %s.' %
+ (timeout, condition_string))
+ time.sleep(poll_interval)
diff --git a/tools/telemetry/telemetry/util_unittest.py b/tools/telemetry/telemetry/util_unittest.py
new file mode 100644
index 0000000..5d144e3
--- /dev/null
+++ b/tools/telemetry/telemetry/util_unittest.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+
+from telemetry import util
+
+class TestWait(unittest.TestCase):
+ @staticmethod
+ def testNonTimeout():
+ def test():
+ return True
+ util.WaitFor(test, 0.1)
+
+ def testTimeout(self):
+ def test():
+ return False
+ self.assertRaises(util.TimeoutException, lambda: util.WaitFor(test, 0.1))
diff --git a/tools/telemetry/telemetry/websocket.py b/tools/telemetry/telemetry/websocket.py
new file mode 100644
index 0000000..a057d1f
--- /dev/null
+++ b/tools/telemetry/telemetry/websocket.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+from __future__ import absolute_import
+import os
+import sys
+
+def __init__():
+ ws_path = os.path.join(os.path.dirname(__file__),
+ '../third_party/websocket-client')
+ assert os.path.exists(os.path.join(ws_path, 'websocket.py'))
+ sys.path.append(ws_path)
+
+__init__()
+
+from websocket import create_connection # pylint: disable=W0611
diff --git a/tools/telemetry/telemetry/websocket_unittest.py b/tools/telemetry/telemetry/websocket_unittest.py
new file mode 100644
index 0000000..826e230
--- /dev/null
+++ b/tools/telemetry/telemetry/websocket_unittest.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import unittest
+
+from telemetry import websocket
+
+class TestWebSocket(unittest.TestCase):
+ def testExports(self):
+ self.assertNotEqual(websocket.create_connection, None)
diff --git a/tools/telemetry/telemetry/wpr_modes.py b/tools/telemetry/telemetry/wpr_modes.py
new file mode 100644
index 0000000..64ffe08
--- /dev/null
+++ b/tools/telemetry/telemetry/wpr_modes.py
@@ -0,0 +1,7 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+WPR_OFF = 'wpr-off'
+WPR_RECORD = 'wpr-record'
+WPR_REPLAY = 'wpr-replay'
+
diff --git a/tools/telemetry/telemetry/wpr_server.py b/tools/telemetry/telemetry/wpr_server.py
new file mode 100644
index 0000000..9789d95
--- /dev/null
+++ b/tools/telemetry/telemetry/wpr_server.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import os
+import sys
+
+# Get chrome/test/functional scripts into our path.
+# TODO(tonyg): Move webpagereplay.py to a common location.
+sys.path.append(
+ os.path.abspath(
+ os.path.join(os.path.dirname(__file__),
+ '../../../chrome/test/functional')))
+import webpagereplay # pylint: disable=F0401
+
+CHROME_FLAGS = webpagereplay.CHROME_FLAGS
+
+class ReplayServer(object):
+ def __init__(self, browser_backend, path, is_record_mode):
+ self._browser_backend = browser_backend
+ self._forwarder = None
+ self._web_page_replay = None
+ self._is_record_mode = is_record_mode
+
+ self._forwarder = browser_backend.CreateForwarder(
+ webpagereplay.HTTP_PORT, webpagereplay.HTTPS_PORT)
+
+ options = []
+ if self._is_record_mode:
+ options.append('--record')
+ if not browser_backend.options.wpr_make_javascript_deterministic:
+ options.append('--inject_scripts=')
+ self._web_page_replay = webpagereplay.ReplayServer(path, options)
+ self._web_page_replay.StartServer()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.Close()
+
+ def Close(self):
+ if self._forwarder:
+ self._forwarder.Close()
+ self._forwarder = None
+ if self._web_page_replay:
+ self._web_page_replay.StopServer()
+ self._web_page_replay = None
diff --git a/tools/telemetry/third_party/websocket-client/.gitignore b/tools/telemetry/third_party/websocket-client/.gitignore
new file mode 100644
index 0000000..c7d73ad
--- /dev/null
+++ b/tools/telemetry/third_party/websocket-client/.gitignore
@@ -0,0 +1,8 @@
+*.pyc
+*~
+*\#
+.\#*
+
+build
+dist
+websocket_client.egg-info
diff --git a/tools/telemetry/third_party/websocket-client/LICENSE b/tools/telemetry/third_party/websocket-client/LICENSE
new file mode 100644
index 0000000..c255f4a
--- /dev/null
+++ b/tools/telemetry/third_party/websocket-client/LICENSE
@@ -0,0 +1,506 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
diff --git a/tools/telemetry/third_party/websocket-client/README.chromium b/tools/telemetry/third_party/websocket-client/README.chromium
new file mode 100644
index 0000000..d5b6b0e
--- /dev/null
+++ b/tools/telemetry/third_party/websocket-client/README.chromium
@@ -0,0 +1,20 @@
+Name: Python websocket-client
+Short Name: websocket-client
+URL: https://github.com/liris/websocket-client
+Version: 0
+Revision: 861f9cf354833fe3992315b60292865c5245c821
+Date: Tue Jul 10 19:57:00 2012 -0700
+License: LGPL-2.1
+License File: NOT_SHIPPED
+Security Critical: no
+
+Description:
+
+websocket-client module is WebSocket client for python. This provide the low
+level APIs for WebSocket. All APIs are the synchronous functions.
+
+Used by the python code in devtools-auto to communicate with a running Chrome instance.
+
+Local Modifications:
+None. However, test, example and packaging code from the upstream repository has
+not been copied downstream. \ No newline at end of file
diff --git a/tools/telemetry/third_party/websocket-client/README.rst b/tools/telemetry/third_party/websocket-client/README.rst
new file mode 100644
index 0000000..1b7faeb
--- /dev/null
+++ b/tools/telemetry/third_party/websocket-client/README.rst
@@ -0,0 +1,140 @@
+=================
+websocket-client
+=================
+
+websocket-client module is WebSocket client for python. This provide the low level APIs for WebSocket. All APIs are the synchronous functions.
+
+websocket-client supports only hybi-13.
+
+License
+============
+
+ - LGPL
+
+Installation
+=============
+
+This module is tested on only Python 2.7.
+
+Type "python setup.py install" or "pip install websocket-client" to install.
+
+This module does not depend on any other module.
+
+Example
+============
+
+Low Level API example::
+
+ from websocket import create_connection
+ ws = create_connection("ws://echo.websocket.org/")
+ print "Sending 'Hello, World'..."
+ ws.send("Hello, World")
+ print "Sent"
+ print "Reeiving..."
+ result = ws.recv()
+ print "Received '%s'" % result
+ ws.close()
+
+
+JavaScript websocket-like API example::
+
+ import websocket
+ import thread
+ import time
+
+ def on_message(ws, message):
+ print message
+
+ def on_error(ws, error):
+ print error
+
+ def on_close(ws):
+ print "### closed ###"
+
+ def on_open(ws):
+ def run(*args):
+ for i in range(3):
+ time.sleep(1)
+ ws.send("Hello %d" % i)
+ time.sleep(1)
+ ws.close()
+ print "thread terminating..."
+ thread.start_new_thread(run, ())
+
+
+ if __name__ == "__main__":
+ websocket.enableTrace(True)
+ ws = websocket.WebSocketApp("ws://echo.websocket.org/",
+ on_message = on_message,
+ on_error = on_error,
+ on_close = on_close)
+ ws.on_open = on_open
+
+ ws.run_forever()
+
+
+wsdump.py
+============
+
+wsdump.py is simple WebSocket test(debug) tool.
+
+sample for echo.websocket.org::
+
+ $ wsdump.py ws://echo.websocket.org/
+ Press Ctrl+C to quit
+ > Hello, WebSocket
+ < Hello, WebSocket
+ > How are you?
+ < How are you?
+
+Usage
+---------
+
+usage::
+ wsdump.py [-h] [-v [VERBOSE]] ws_url
+
+WebSocket Simple Dump Tool
+
+positional arguments:
+ ws_url websocket url. ex. ws://echo.websocket.org/
+
+optional arguments:
+ -h, --help show this help message and exit
+
+ -v VERBOSE, --verbose VERBOSE set verbose mode. If set to 1, show opcode. If set to 2, enable to trace websocket module
+
+example::
+
+ $ wsdump.py ws://echo.websocket.org/
+ $ wsdump.py ws://echo.websocket.org/ -v
+ $ wsdump.py ws://echo.websocket.org/ -vv
+
+ChangeLog
+============
+
+- v0.7.0
+
+ - fixed problem to read long data.(ISSUE#12)
+ - fix buffer size boundary violation
+
+- v0.6.0
+
+ - Patches: UUID4, self.keep_running, mask_key (ISSUE#11)
+ - add wsdump.py tool
+
+- v0.5.2
+
+ - fix Echo App Demo Throw Error: 'NoneType' object has no attribute 'opcode (ISSUE#10)
+
+- v0.5.1
+
+ - delete invalid print statement.
+
+- v0.5.0
+
+ - support hybi-13 protocol.
+
+- v0.4.1
+
+ - fix incorrect custom header order(ISSUE#1)
+
diff --git a/tools/telemetry/third_party/websocket-client/websocket.py b/tools/telemetry/third_party/websocket-client/websocket.py
new file mode 100644
index 0000000..480bfc0
--- /dev/null
+++ b/tools/telemetry/third_party/websocket-client/websocket.py
@@ -0,0 +1,756 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+"""
+
+
+import socket
+from urlparse import urlparse
+import os
+import struct
+import uuid
+import hashlib
+import base64
+import logging
+
+"""
+websocket python client.
+=========================
+
+This version support only hybi-13.
+Please see http://tools.ietf.org/html/rfc6455 for protocol.
+"""
+
+
+# websocket supported version.
+VERSION = 13
+
+# closing frame status codes.
+STATUS_NORMAL = 1000
+STATUS_GOING_AWAY = 1001
+STATUS_PROTOCOL_ERROR = 1002
+STATUS_UNSUPPORTED_DATA_TYPE = 1003
+STATUS_STATUS_NOT_AVAILABLE = 1005
+STATUS_ABNORMAL_CLOSED = 1006
+STATUS_INVALID_PAYLOAD = 1007
+STATUS_POLICY_VIOLATION = 1008
+STATUS_MESSAGE_TOO_BIG = 1009
+STATUS_INVALID_EXTENSION = 1010
+STATUS_UNEXPECTED_CONDITION = 1011
+STATUS_TLS_HANDSHAKE_ERROR = 1015
+
+logger = logging.getLogger()
+
+class WebSocketException(Exception):
+ """
+ websocket exeception class.
+ """
+ pass
+
+class WebSocketConnectionClosedException(WebSocketException):
+ """
+ If remote host closed the connection or some network error happened,
+ this exception will be raised.
+ """
+ pass
+
+default_timeout = None
+traceEnabled = False
+
+def enableTrace(tracable):
+ """
+ turn on/off the tracability.
+
+ tracable: boolean value. if set True, tracability is enabled.
+ """
+ global traceEnabled
+ traceEnabled = tracable
+ if tracable:
+ if not logger.handlers:
+ logger.addHandler(logging.StreamHandler())
+ logger.setLevel(logging.DEBUG)
+
+def setdefaulttimeout(timeout):
+ """
+ Set the global timeout setting to connect.
+
+ timeout: default socket timeout time. This value is second.
+ """
+ global default_timeout
+ default_timeout = timeout
+
+def getdefaulttimeout():
+ """
+ Return the global timeout setting(second) to connect.
+ """
+ return default_timeout
+
+def _parse_url(url):
+ """
+ parse url and the result is tuple of
+ (hostname, port, resource path and the flag of secure mode)
+
+ url: url string.
+ """
+ if ":" not in url:
+ raise ValueError("url is invalid")
+
+ scheme, url = url.split(":", 1)
+
+ parsed = urlparse(url, scheme="http")
+ if parsed.hostname:
+ hostname = parsed.hostname
+ else:
+ raise ValueError("hostname is invalid")
+ port = 0
+ if parsed.port:
+ port = parsed.port
+
+ is_secure = False
+ if scheme == "ws":
+ if not port:
+ port = 80
+ elif scheme == "wss":
+ is_secure = True
+ if not port:
+ port = 443
+ else:
+ raise ValueError("scheme %s is invalid" % scheme)
+
+ if parsed.path:
+ resource = parsed.path
+ else:
+ resource = "/"
+
+ if parsed.query:
+ resource += "?" + parsed.query
+
+ return (hostname, port, resource, is_secure)
+
+def create_connection(url, timeout=None, **options):
+ """
+ connect to url and return websocket object.
+
+ Connect to url and return the WebSocket object.
+ Passing optional timeout parameter will set the timeout on the socket.
+ If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used.
+ You can customize using 'options'.
+ If you set "header" dict object, you can set your own custom header.
+
+ >>> conn = create_connection("ws://echo.websocket.org/",
+ ... header={"User-Agent: MyProgram",
+ ... "x-custom: header"})
+
+
+ timeout: socket timeout time. This value is integer.
+ if you set None for this value, it means "use default_timeout value"
+
+ options: current support option is only "header".
+ if you set header as dict value, the custom HTTP headers are added.
+ """
+ websock = WebSocket()
+ websock.settimeout(timeout != None and timeout or default_timeout)
+ websock.connect(url, **options)
+ return websock
+
+_MAX_INTEGER = (1 << 32) -1
+_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
+_MAX_CHAR_BYTE = (1<<8) -1
+
+# ref. Websocket gets an update, and it breaks stuff.
+# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html
+
+def _create_sec_websocket_key():
+ uid = uuid.uuid4()
+ return base64.encodestring(uid.bytes).strip()
+
+_HEADERS_TO_CHECK = {
+ "upgrade": "websocket",
+ "connection": "upgrade",
+ }
+
+class _SSLSocketWrapper(object):
+ def __init__(self, sock):
+ self.ssl = socket.ssl(sock)
+
+ def recv(self, bufsize):
+ return self.ssl.read(bufsize)
+
+ def send(self, payload):
+ return self.ssl.write(payload)
+
+_BOOL_VALUES = (0, 1)
+def _is_bool(*values):
+ for v in values:
+ if v not in _BOOL_VALUES:
+ return False
+
+ return True
+
+class ABNF(object):
+ """
+ ABNF frame class.
+ see http://tools.ietf.org/html/rfc5234
+ and http://tools.ietf.org/html/rfc6455#section-5.2
+ """
+
+ # operation code values.
+ OPCODE_TEXT = 0x1
+ OPCODE_BINARY = 0x2
+ OPCODE_CLOSE = 0x8
+ OPCODE_PING = 0x9
+ OPCODE_PONG = 0xa
+
+ # available operation code value tuple
+ OPCODES = (OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
+ OPCODE_PING, OPCODE_PONG)
+
+ # opcode human readable string
+ OPCODE_MAP = {
+ OPCODE_TEXT: "text",
+ OPCODE_BINARY: "binary",
+ OPCODE_CLOSE: "close",
+ OPCODE_PING: "ping",
+ OPCODE_PONG: "pong"
+ }
+
+ # data length threashold.
+ LENGTH_7 = 0x7d
+ LENGTH_16 = 1 << 16
+ LENGTH_63 = 1 << 63
+
+ def __init__(self, fin = 0, rsv1 = 0, rsv2 = 0, rsv3 = 0,
+ opcode = OPCODE_TEXT, mask = 1, data = ""):
+ """
+ Constructor for ABNF.
+ please check RFC for arguments.
+ """
+ self.fin = fin
+ self.rsv1 = rsv1
+ self.rsv2 = rsv2
+ self.rsv3 = rsv3
+ self.opcode = opcode
+ self.mask = mask
+ self.data = data
+ self.get_mask_key = os.urandom
+
+ @staticmethod
+ def create_frame(data, opcode):
+ """
+ create frame to send text, binary and other data.
+
+ data: data to send. This is string value(byte array).
+ if opcode is OPCODE_TEXT and this value is uniocde,
+ data value is conveted into unicode string, automatically.
+
+ opcode: operation code. please see OPCODE_XXX.
+ """
+ if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
+ data = data.encode("utf-8")
+ # mask must be set if send data from client
+ return ABNF(1, 0, 0, 0, opcode, 1, data)
+
+ def format(self):
+ """
+ format this object to string(byte array) to send data to server.
+ """
+ if not _is_bool(self.fin, self.rsv1, self.rsv2, self.rsv3):
+ raise ValueError("not 0 or 1")
+ if self.opcode not in ABNF.OPCODES:
+ raise ValueError("Invalid OPCODE")
+ length = len(self.data)
+ if length >= ABNF.LENGTH_63:
+ raise ValueError("data is too long")
+
+ frame_header = chr(self.fin << 7
+ | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4
+ | self.opcode)
+ if length < ABNF.LENGTH_7:
+ frame_header += chr(self.mask << 7 | length)
+ elif length < ABNF.LENGTH_16:
+ frame_header += chr(self.mask << 7 | 0x7e)
+ frame_header += struct.pack("!H", length)
+ else:
+ frame_header += chr(self.mask << 7 | 0x7f)
+ frame_header += struct.pack("!Q", length)
+
+ if not self.mask:
+ return frame_header + self.data
+ else:
+ mask_key = self.get_mask_key(4)
+ return frame_header + self._get_masked(mask_key)
+
+ def _get_masked(self, mask_key):
+ s = ABNF.mask(mask_key, self.data)
+ return mask_key + "".join(s)
+
+ @staticmethod
+ def mask(mask_key, data):
+ """
+ mask or unmask data. Just do xor for each byte
+
+ mask_key: 4 byte string(byte).
+
+ data: data to mask/unmask.
+ """
+ _m = map(ord, mask_key)
+ _d = map(ord, data)
+ for i in range(len(_d)):
+ _d[i] ^= _m[i % 4]
+ s = map(chr, _d)
+ return "".join(s)
+
+class WebSocket(object):
+ """
+ Low level WebSocket interface.
+ This class is based on
+ The WebSocket protocol draft-hixie-thewebsocketprotocol-76
+ http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+
+ We can connect to the websocket server and send/recieve data.
+ The following example is a echo client.
+
+ >>> import websocket
+ >>> ws = websocket.WebSocket()
+ >>> ws.connect("ws://echo.websocket.org")
+ >>> ws.send("Hello, Server")
+ >>> ws.recv()
+ 'Hello, Server'
+ >>> ws.close()
+
+ get_mask_key: a callable to produce new mask keys, see the set_mask_key
+ function's docstring for more details
+ """
+ def __init__(self, get_mask_key = None):
+ """
+ Initalize WebSocket object.
+ """
+ self.connected = False
+ self.io_sock = self.sock = socket.socket()
+ self.get_mask_key = get_mask_key
+
+ def set_mask_key(self, func):
+ """
+ set function to create musk key. You can custumize mask key generator.
+ Mainly, this is for testing purpose.
+
+ func: callable object. the fuct must 1 argument as integer.
+ The argument means length of mask key.
+ This func must be return string(byte array),
+ which length is argument specified.
+ """
+ self.get_mask_key = func
+
+ def settimeout(self, timeout):
+ """
+ Set the timeout to the websocket.
+
+ timeout: timeout time(second).
+ """
+ self.sock.settimeout(timeout)
+
+ def gettimeout(self):
+ """
+ Get the websocket timeout(second).
+ """
+ return self.sock.gettimeout()
+
+ def connect(self, url, **options):
+ """
+ Connect to url. url is websocket url scheme. ie. ws://host:port/resource
+ You can customize using 'options'.
+ If you set "header" dict object, you can set your own custom header.
+
+ >>> ws = WebSocket()
+ >>> ws.connect("ws://echo.websocket.org/",
+ ... header={"User-Agent: MyProgram",
+ ... "x-custom: header"})
+
+ timeout: socket timeout time. This value is integer.
+ if you set None for this value,
+ it means "use default_timeout value"
+
+ options: current support option is only "header".
+ if you set header as dict value,
+ the custom HTTP headers are added.
+
+ """
+ hostname, port, resource, is_secure = _parse_url(url)
+ # TODO: we need to support proxy
+ self.sock.connect((hostname, port))
+ if is_secure:
+ self.io_sock = _SSLSocketWrapper(self.sock)
+ self._handshake(hostname, port, resource, **options)
+
+ def _handshake(self, host, port, resource, **options):
+ sock = self.io_sock
+ headers = []
+ headers.append("GET %s HTTP/1.1" % resource)
+ headers.append("Upgrade: websocket")
+ headers.append("Connection: Upgrade")
+ if port == 80:
+ hostport = host
+ else:
+ hostport = "%s:%d" % (host, port)
+ headers.append("Host: %s" % hostport)
+ headers.append("Origin: %s" % hostport)
+
+ key = _create_sec_websocket_key()
+ headers.append("Sec-WebSocket-Key: %s" % key)
+ headers.append("Sec-WebSocket-Version: %s" % VERSION)
+ if "header" in options:
+ headers.extend(options["header"])
+
+ headers.append("")
+ headers.append("")
+
+ header_str = "\r\n".join(headers)
+ sock.send(header_str)
+ if traceEnabled:
+ logger.debug( "--- request header ---")
+ logger.debug( header_str)
+ logger.debug("-----------------------")
+
+ status, resp_headers = self._read_headers()
+ if status != 101:
+ self.close()
+ raise WebSocketException("Handshake Status %d" % status)
+
+ success = self._validate_header(resp_headers, key)
+ if not success:
+ self.close()
+ raise WebSocketException("Invalid WebSocket Header")
+
+ self.connected = True
+
+ def _validate_header(self, headers, key):
+ for k, v in _HEADERS_TO_CHECK.iteritems():
+ r = headers.get(k, None)
+ if not r:
+ return False
+ r = r.lower()
+ if v != r:
+ return False
+
+ result = headers.get("sec-websocket-accept", None)
+ if not result:
+ return False
+ result = result.lower()
+
+ value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+ hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower()
+ return hashed == result
+
+ def _read_headers(self):
+ status = None
+ headers = {}
+ if traceEnabled:
+ logger.debug("--- response header ---")
+
+ while True:
+ line = self._recv_line()
+ if line == "\r\n":
+ break
+ line = line.strip()
+ if traceEnabled:
+ logger.debug(line)
+ if not status:
+ status_info = line.split(" ", 2)
+ status = int(status_info[1])
+ else:
+ kv = line.split(":", 1)
+ if len(kv) == 2:
+ key, value = kv
+ headers[key.lower()] = value.strip().lower()
+ else:
+ raise WebSocketException("Invalid header")
+
+ if traceEnabled:
+ logger.debug("-----------------------")
+
+ return status, headers
+
+ def send(self, payload, opcode = ABNF.OPCODE_TEXT):
+ """
+ Send the data as string.
+
+ payload: Payload must be utf-8 string or unicoce,
+ if the opcode is OPCODE_TEXT.
+ Otherwise, it must be string(byte array)
+
+ opcode: operation code to send. Please see OPCODE_XXX.
+ """
+ frame = ABNF.create_frame(payload, opcode)
+ if self.get_mask_key:
+ frame.get_mask_key = self.get_mask_key
+ data = frame.format()
+ self.io_sock.send(data)
+ if traceEnabled:
+ logger.debug("send: " + repr(data))
+
+ def ping(self, payload = ""):
+ """
+ send ping data.
+
+ payload: data payload to send server.
+ """
+ self.send(payload, ABNF.OPCODE_PING)
+
+ def pong(self, payload):
+ """
+ send pong data.
+
+ payload: data payload to send server.
+ """
+ self.send(payload, ABNF.OPCODE_PONG)
+
+ def recv(self):
+ """
+ Receive string data(byte array) from the server.
+
+ return value: string(byte array) value.
+ """
+ opcode, data = self.recv_data()
+ return data
+
+ def recv_data(self):
+ """
+ Recieve data with operation code.
+
+ return value: tuple of operation code and string(byte array) value.
+ """
+ while True:
+ frame = self.recv_frame()
+ if not frame:
+ # handle error:
+ # 'NoneType' object has no attribute 'opcode'
+ raise WebSocketException("Not a valid frame %s" % frame)
+ elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
+ return (frame.opcode, frame.data)
+ elif frame.opcode == ABNF.OPCODE_CLOSE:
+ self.send_close()
+ return (frame.opcode, None)
+ elif frame.opcode == ABNF.OPCODE_PING:
+ self.pong("Hi!")
+
+
+ def recv_frame(self):
+ """
+ recieve data as frame from server.
+
+ return value: ABNF frame object.
+ """
+ header_bytes = self._recv(2)
+ if not header_bytes:
+ return None
+ b1 = ord(header_bytes[0])
+ fin = b1 >> 7 & 1
+ rsv1 = b1 >> 6 & 1
+ rsv2 = b1 >> 5 & 1
+ rsv3 = b1 >> 4 & 1
+ opcode = b1 & 0xf
+ b2 = ord(header_bytes[1])
+ mask = b2 >> 7 & 1
+ length = b2 & 0x7f
+
+ length_data = ""
+ if length == 0x7e:
+ length_data = self._recv(2)
+ length = struct.unpack("!H", length_data)[0]
+ elif length == 0x7f:
+ length_data = self._recv(8)
+ length = struct.unpack("!Q", length_data)[0]
+
+ mask_key = ""
+ if mask:
+ mask_key = self._recv(4)
+ data = self._recv_strict(length)
+ if traceEnabled:
+ recieved = header_bytes + length_data + mask_key + data
+ logger.debug("recv: " + repr(recieved))
+
+ if mask:
+ data = ABNF.mask(mask_key, data)
+
+ frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, mask, data)
+ return frame
+
+ def send_close(self, status = STATUS_NORMAL, reason = ""):
+ """
+ send close data to the server.
+
+ status: status code to send. see STATUS_XXX.
+
+ reason: the reason to close. This must be string.
+ """
+ if status < 0 or status >= ABNF.LENGTH_16:
+ raise ValueError("code is invalid range")
+ self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
+
+
+
+ def close(self, status = STATUS_NORMAL, reason = ""):
+ """
+ Close Websocket object
+
+ status: status code to send. see STATUS_XXX.
+
+ reason: the reason to close. This must be string.
+ """
+ if self.connected:
+ if status < 0 or status >= ABNF.LENGTH_16:
+ raise ValueError("code is invalid range")
+
+ try:
+ self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
+ timeout = self.sock.gettimeout()
+ self.sock.settimeout(3)
+ try:
+ frame = self.recv_frame()
+ if logger.isEnabledFor(logging.DEBUG):
+ logger.error("close status: " + repr(frame.data))
+ except:
+ pass
+ self.sock.settimeout(timeout)
+ self.sock.shutdown(socket.SHUT_RDWR)
+ except:
+ pass
+ self._closeInternal()
+
+ def _closeInternal(self):
+ self.connected = False
+ self.sock.close()
+ self.io_sock = self.sock
+
+ def _recv(self, bufsize):
+ bytes = self.io_sock.recv(bufsize)
+ if bytes == 0:
+ raise WebSocketConnectionClosedException()
+ return bytes
+
+ def _recv_strict(self, bufsize):
+ remaining = bufsize
+ bytes = ""
+ while remaining:
+ bytes += self._recv(remaining)
+ remaining = bufsize - len(bytes)
+
+ return bytes
+
+ def _recv_line(self):
+ line = []
+ while True:
+ c = self._recv(1)
+ line.append(c)
+ if c == "\n":
+ break
+ return "".join(line)
+
+class WebSocketApp(object):
+ """
+ Higher level of APIs are provided.
+ The interface is like JavaScript WebSocket object.
+ """
+ def __init__(self, url,
+ on_open = None, on_message = None, on_error = None,
+ on_close = None, keep_running = True, get_mask_key = None):
+ """
+ url: websocket url.
+ on_open: callable object which is called at opening websocket.
+ this function has one argument. The arugment is this class object.
+ on_message: callbale object which is called when recieved data.
+ on_message has 2 arguments.
+ The 1st arugment is this class object.
+ The passing 2nd arugment is utf-8 string which we get from the server.
+ on_error: callable object which is called when we get error.
+ on_error has 2 arguments.
+ The 1st arugment is this class object.
+ The passing 2nd arugment is exception object.
+ on_close: callable object which is called when closed the connection.
+ this function has one argument. The arugment is this class object.
+ keep_running: a boolean flag indicating whether the app's main loop should
+ keep running, defaults to True
+ get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's
+ docstring for more information
+ """
+ self.url = url
+ self.on_open = on_open
+ self.on_message = on_message
+ self.on_error = on_error
+ self.on_close = on_close
+ self.keep_running = keep_running
+ self.get_mask_key = get_mask_key
+ self.sock = None
+
+ def send(self, data):
+ """
+ send message. data must be utf-8 string or unicode.
+ """
+ if self.sock.send(data) == 0:
+ raise WebSocketConnectionClosedException()
+
+ def close(self):
+ """
+ close websocket connection.
+ """
+ self.keep_running = False
+ self.sock.close()
+
+ def run_forever(self):
+ """
+ run event loop for WebSocket framework.
+ This loop is infinite loop and is alive during websocket is available.
+ """
+ if self.sock:
+ raise WebSocketException("socket is already opened")
+ try:
+ self.sock = WebSocket(self.get_mask_key)
+ self.sock.connect(self.url)
+ self._run_with_no_err(self.on_open)
+ while self.keep_running:
+ data = self.sock.recv()
+ if data is None:
+ break
+ self._run_with_no_err(self.on_message, data)
+ except Exception, e:
+ self._run_with_no_err(self.on_error, e)
+ finally:
+ self.sock.close()
+ self._run_with_no_err(self.on_close)
+ self.sock = None
+
+ def _run_with_no_err(self, callback, *args):
+ if callback:
+ try:
+ callback(self, *args)
+ except Exception, e:
+ if logger.isEnabledFor(logging.DEBUG):
+ logger.error(e)
+
+
+if __name__ == "__main__":
+ enableTrace(True)
+ ws = create_connection("ws://echo.websocket.org/")
+ print "Sending 'Hello, World'..."
+ ws.send("Hello, World")
+ print "Sent"
+ print "Receiving..."
+ result = ws.recv()
+ print "Received '%s'" % result
+ ws.close()
diff --git a/tools/telemetry/unittest_data/.gitignore b/tools/telemetry/unittest_data/.gitignore
new file mode 100644
index 0000000..4842ffe
--- /dev/null
+++ b/tools/telemetry/unittest_data/.gitignore
@@ -0,0 +1 @@
+internal \ No newline at end of file
diff --git a/tools/telemetry/unittest_data/blank.html b/tools/telemetry/unittest_data/blank.html
new file mode 100644
index 0000000..8d0ce09
--- /dev/null
+++ b/tools/telemetry/unittest_data/blank.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+Hello world
+</body>
+</html>
diff --git a/tools/telemetry/unittest_data/non_scrollable_page.html b/tools/telemetry/unittest_data/non_scrollable_page.html
new file mode 100644
index 0000000..5770408
--- /dev/null
+++ b/tools/telemetry/unittest_data/non_scrollable_page.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+ <head>
+ <style type="text/css">
+ </style>
+ </head>
+ <body>
+ Hello, world.
+ </body>
+</html>
diff --git a/tools/telemetry/unittest_data/page_that_logs_to_console.html b/tools/telemetry/unittest_data/page_that_logs_to_console.html
new file mode 100644
index 0000000..373eebc
--- /dev/null
+++ b/tools/telemetry/unittest_data/page_that_logs_to_console.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+<script>
+ window.__logCount = 0;
+ setInterval(function() {
+ console.log("Hello, world")
+ window.__logCount += 1
+ }, 100);
+</script>
+</body>
+</html>
diff --git a/tools/telemetry/unittest_data/scrollable_page.html b/tools/telemetry/unittest_data/scrollable_page.html
new file mode 100644
index 0000000..c5102ce
--- /dev/null
+++ b/tools/telemetry/unittest_data/scrollable_page.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html>
+ <head>
+ <style type="text/css">
+ body { height: 1500px; }
+ </style>
+ </head>
+ <body></body>
+</html>