diff options
author | dtu@chromium.org <dtu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-08 08:17:22 +0000 |
---|---|---|
committer | dtu@chromium.org <dtu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-08 08:17:22 +0000 |
commit | eea2e67034a223c74ce0e8c0517f6066cc1ee143 (patch) | |
tree | 64c4082a9d8e3c0df8fa64f689c7fe7a09b0edbb /tools/telemetry | |
parent | 5971ec68da0d265e7d87352dfc4cec23e65c4a2f (diff) | |
download | chromium_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')
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> |