├── .gitignore ├── images ├── woot.png └── favicon.ico ├── .gitmodules ├── templates ├── ads.html ├── error.html ├── hurr.html ├── up.html ├── down.html ├── index.html └── layout.html ├── downer.py ├── cron.yml ├── betterhandler.py ├── test ├── appengine_api_tests.py ├── unit_tests.py └── web_tests.py ├── index.yaml ├── LICENSE ├── app.yaml ├── README.textile ├── downerclear.py ├── tweetcheck.py ├── main.py └── gaeunit.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pyc 3 | .DS_Store -------------------------------------------------------------------------------- /images/woot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al3x/downforeveryoneorjustme/master/images/woot.png -------------------------------------------------------------------------------- /images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al3x/downforeveryoneorjustme/master/images/favicon.ico -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sinatra"] 2 | path = sinatra 3 | url = git://github.com/bmizerany/sinatra.git 4 | -------------------------------------------------------------------------------- /templates/ads.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | We Guarantee Our Uptime! Switch to Site5 Web Hosting! 4 |
-------------------------------------------------------------------------------- /downer.py: -------------------------------------------------------------------------------- 1 | from google.appengine.ext import db 2 | 3 | class Downer(db.Model): 4 | domain = db.StringProperty(required=True) 5 | down_at = db.DateTimeProperty(auto_now_add=True) 6 | -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 | Huh? {{ domain }} doesn't look like a site on the interwho. 4 |

Try again?

5 | {% include "ads.html" %} 6 | {% endblock %} -------------------------------------------------------------------------------- /cron.yml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: tweet on new top down domains 3 | url: /_tweetcheck 4 | schedule: every 10 minutes 5 | 6 | - description: delete old downer data 7 | url: /_downerclear 8 | schedule: every 6 hours 9 | -------------------------------------------------------------------------------- /templates/hurr.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 | If you can see this page and still think we're down, it's just you. 4 |

Check another site?

5 | {% include "ads.html" %} 6 | {% endblock %} -------------------------------------------------------------------------------- /betterhandler.py: -------------------------------------------------------------------------------- 1 | import os 2 | from google.appengine.ext import webapp 3 | 4 | class BetterHandler(webapp.RequestHandler): 5 | def template_path(self, filename): 6 | return os.path.join(os.path.dirname(__file__), 'templates', filename) 7 | -------------------------------------------------------------------------------- /templates/up.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 | It's just you. {{ domain }} is up. 4 |

Check another site?

5 | {% include "ads.html" %} 6 | {% endblock %} -------------------------------------------------------------------------------- /test/appengine_api_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from google.appengine.api import urlfetch 3 | 4 | class AppEngineAPITest(unittest.TestCase): 5 | def test_urlfetch(self): 6 | response = urlfetch.fetch('http://www.google.com') 7 | self.assertEquals(0, response.content.find('')) 8 | -------------------------------------------------------------------------------- /templates/down.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 | It's not just you! {{ domain }} looks down from here. 4 |

Check another site?

5 |
6 |
7 | Tired Of Downtime? Switch to Site5 Web Hosting! 8 |
9 | {% endblock %} -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |
4 | Is 5 | 6 | down for everyone 7 | or just me? 8 | 9 |
10 | {% endblock %} -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 Alex Payne 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /test/unit_tests.py: -------------------------------------------------------------------------------- 1 | import logging, unittest 2 | from main import Url 3 | 4 | class UrlTest(unittest.TestCase): 5 | def test_sane_domain(self): 6 | url = Url('google.com') 7 | self.assertEqual('google.com', url.original_domain) 8 | self.assertEqual('http://google.com', url.domain) 9 | 10 | def test_domain_with_http(self): 11 | url = Url('http://google.com') 12 | self.assertEqual('http://google.com', url.original_domain) 13 | self.assertEqual('http://google.com', url.domain) 14 | 15 | def test_domain_with_http_encoded(self): 16 | url = Url('http%3A//google.com') 17 | self.assertEqual('google.com', url.original_domain) 18 | self.assertEqual('http://google.com', url.domain) -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: downforeveryoneorjustme 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: /favicon.ico 8 | static_files: images/favicon.ico 9 | upload: images/favicon.ico 10 | 11 | - url: /stylesheets 12 | static_dir: stylesheets 13 | 14 | - url: /images 15 | static_dir: images 16 | 17 | - url: /javascripts 18 | static_dir: javascripts 19 | 20 | - url: /test.* 21 | script: gaeunit.py 22 | 23 | - url: /_tweetcheck 24 | script: tweetcheck.py 25 | 26 | - url: /_downerclear 27 | script: downerclear.py 28 | 29 | - url: /remote_api 30 | script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py 31 | login: admin 32 | 33 | - url: .* 34 | script: main.py 35 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. Introduction 2 | 3 | This code _used to_ power http://downforeveryoneorjustme.com, a "single serving site":http://isthisyourpaperonsingleservingsites.com/ that tells you whether or not a web site appears to be down. The author no longer owns or operates this domain. 4 | 5 | h1. Installation (Or, Why You're Probably Not Installing This) 6 | 7 | This code is provided mostly as an example resource for other developers seeking to learn about "Google App Engine":http://code.google.com/appengine/. In the unlikely case that you want to install a clone of my site, complete with advertising and such, feel free to do so I guess. That's pretty weird, dude. 8 | 9 | h1. License 10 | 11 | See the "LICENSE" file in the source code. Included libraries are licensed under their respective licenses. 12 | 13 | h1. Author 14 | 15 | "Alex Payne":http://al3x.net/ 16 | -------------------------------------------------------------------------------- /downerclear.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base64, cgi, logging, urllib, wsgiref.handlers 4 | from datetime import * 5 | from itertools import * 6 | 7 | from downer import * 8 | 9 | from google.appengine.ext import db 10 | from google.appengine.ext import webapp 11 | from google.appengine.ext.webapp import template 12 | from google.appengine.api import urlfetch 13 | 14 | 15 | class DownerClear(webapp.RequestHandler): 16 | def get(self): 17 | hour_ago = datetime.now() + timedelta(minutes=-60) 18 | cleared = 0 19 | 20 | query = db.GqlQuery("SELECT __key__ FROM Downer WHERE down_at < :1", hour_ago) 21 | results = query.fetch(500) 22 | results_size = len(results) 23 | db.delete(results) 24 | cleared += results_size 25 | 26 | return self.response.out.write("cleared %d" % cleared) 27 | 28 | 29 | def main(): 30 | application = webapp.WSGIApplication([('/_downerclear', DownerClear)], 31 | debug=True) 32 | wsgiref.handlers.CGIHandler().run(application) -------------------------------------------------------------------------------- /test/web_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from google.appengine.ext import webapp 4 | from main import FrontPage, CheckDomain 5 | from webtest import TestApp 6 | 7 | class MainTest(unittest.TestCase): 8 | def setUp(self): 9 | self.application = webapp.WSGIApplication([('/', FrontPage)], debug=True) 10 | 11 | def test_front_page(self): 12 | app = TestApp(self.application) 13 | response = app.get('/') 14 | self.assertEqual('200 OK', response.status) 15 | self.assertTrue('down for everyone' in response) 16 | 17 | 18 | class CheckDomainTest(unittest.TestCase): 19 | def setUp(self): 20 | self.application = webapp.WSGIApplication([(r'/(.*)', CheckDomain)], debug=True) 21 | 22 | def test_page_with_param(self): 23 | app = TestApp(self.application) 24 | response = app.get('/google.com') 25 | self.assertEqual('200 OK', response.status) 26 | self.assertTrue('google.com' in response) 27 | 28 | def test_known_bad_domain(self): 29 | app = TestApp(self.application) 30 | response = app.get('/cse.ohio-state.edu') 31 | self.assertEqual('200 OK', response.status) 32 | self.assertTrue('cse.ohio-state.edu' in response) 33 | self.assertTrue('down' in response) 34 | 35 | def test_amazon(self): 36 | app = TestApp(self.application) 37 | response = app.get('/amazon.com') 38 | self.assertEqual('200 OK', response.status) 39 | self.assertTrue('amazon.com' in response) 40 | self.assertFalse('interwho' in response) 41 | 42 | def test_google_with_http(self): 43 | app = TestApp(self.application) 44 | response = app.get('/http://google.com') 45 | self.assertEqual('200 OK', response.status) 46 | self.assertTrue('google.com' in response) 47 | self.assertFalse('interwho' in response) -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title }} 8 | 18 | 31 | 32 | 33 |
34 | {% block content %}{% endblock %} 35 |
36 | 37 | 38 | 41 | 45 | 46 | -------------------------------------------------------------------------------- /tweetcheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base64, cgi, logging, urllib, wsgiref.handlers 4 | from datetime import * 5 | from itertools import * 6 | 7 | from downer import * 8 | 9 | from google.appengine.ext import db 10 | from google.appengine.ext import webapp 11 | from google.appengine.ext.webapp import template 12 | from google.appengine.api import memcache 13 | from google.appengine.api import urlfetch 14 | 15 | class DownError(Exception): 16 | pass 17 | 18 | def get_top_domain(): 19 | minutes_ago = datetime.now() + timedelta(minutes=-10) 20 | query = db.GqlQuery("SELECT * FROM Downer WHERE down_at >= :1", minutes_ago) 21 | results = query.fetch(1000) 22 | 23 | domains = [] 24 | 25 | for result in results: 26 | domains.append(result.domain) 27 | 28 | grouped_domains = dict([(a, len(list(b))) for a, b in groupby(sorted(domains))]) 29 | sorted_domains = sorted(grouped_domains.items(), reverse=True) 30 | 31 | if len(sorted_domains) > 0: 32 | return sorted_domains[0][0] 33 | else: 34 | raise DownError, "No domains recorded down" 35 | 36 | 37 | def tweet(msg): 38 | url = "https://twitter.com/statuses/update.json" 39 | username = "makeuparealusername" 40 | password = "pickagoodpassword" 41 | 42 | form_fields = { "status": msg } 43 | payload = urllib.urlencode(form_fields) 44 | 45 | authheader = "Basic %s" % base64.encodestring('%s:%s' % (username, password)) 46 | base64string = base64.encodestring('%s:%s' % (username, password))[:-1] 47 | authheader = "Basic %s" % base64string 48 | 49 | result = urlfetch.fetch(url=url, payload=payload, method=urlfetch.POST, 50 | headers={"Authorization": authheader}) 51 | 52 | if int(result.status_code) == 200: 53 | return 200 54 | else: 55 | raise DownError, result.status_code 56 | 57 | 58 | class TweetCheck(webapp.RequestHandler): 59 | def get(self): 60 | try: 61 | current_top_domain = get_top_domain() 62 | except DownError, e: 63 | return self.response.out.write("Exception: %s" % e) 64 | 65 | last_top_domain = memcache.get("topdomain") 66 | 67 | if current_top_domain != last_top_domain: 68 | memcache.set("topdomain", current_top_domain) 69 | 70 | try: 71 | tweet("%s looks like it might be down." % current_top_domain) 72 | except DownError, e: 73 | return self.response.out.write("Exception: couldn't tweet, got a %s" % e) 74 | 75 | return self.response.out.write("New top domain: %s" % current_top_domain) 76 | else: 77 | return self.response.out.write("Same old top domain: %s" % last_top_domain) 78 | 79 | 80 | def main(): 81 | application = webapp.WSGIApplication([('/_tweetcheck', TweetCheck)], 82 | debug=True) 83 | wsgiref.handlers.CGIHandler().run(application) 84 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import cgi, logging, re, wsgiref.handlers 4 | 5 | from betterhandler import * 6 | from downer import * 7 | 8 | from google.appengine.ext import webapp 9 | from google.appengine.ext.webapp import template 10 | from google.appengine.api import urlfetch 11 | from google.appengine.api import memcache 12 | 13 | try: 14 | from google.appengine.runtime import DeadlineExceededError 15 | except ImportError: 16 | from google.appengine.runtime.apiproxy_errors import DeadlineExceededError 17 | 18 | HTTPRE = re.compile('http:\/\/') 19 | DOWNRE = re.compile('downforeveryoneorjustme') 20 | DOMRE = re.compile("\.\w{2,20}") 21 | 22 | def valid_response_code(code): 23 | if (code == 200) or (code == 301) or (code == 302): 24 | return True 25 | else: 26 | return False 27 | 28 | class Url: 29 | def __init__(self, domain): 30 | if domain.find("http%3A//") is not -1: 31 | domain = domain.split("http%3A//")[1] 32 | 33 | self.original_domain = domain 34 | self.domain = self.clean_url(domain) 35 | logging.debug("new Url: ", self.original_domain, self.domain) 36 | 37 | def clean_url(self, domain): 38 | domain = cgi.escape(domain) 39 | domain.encode("utf-8") 40 | 41 | if HTTPRE.match(domain) == None: 42 | domain = 'http://' + domain 43 | 44 | pieces = domain.split("/") 45 | 46 | while (len(pieces) > 3): 47 | pieces.pop() 48 | 49 | domain = "/".join(pieces) 50 | 51 | return domain 52 | 53 | def dos(self): 54 | doscheck = memcache.get(self.domain) 55 | 56 | if doscheck is not None: 57 | doscheck = memcache.incr(self.domain) 58 | else: 59 | doscheck = memcache.add(self.domain, 0, 60) 60 | 61 | if not doscheck: 62 | logging.error("Memcache set failed.") 63 | 64 | doscheck = 0 65 | 66 | if doscheck > 500: 67 | return True 68 | else: 69 | return False 70 | 71 | def isself(self): 72 | logging.debug("in isself domain is %s", self.domain) 73 | 74 | if DOWNRE.search(self.domain) == None: 75 | return False 76 | else: 77 | return True 78 | 79 | def missingdomain(self): 80 | if DOMRE.search(self.domain) == None: 81 | return True 82 | else: 83 | return False 84 | 85 | class FrontPage(BetterHandler): 86 | def get(self): 87 | for_template = { 88 | 'title': 'Down for everyone or just me?', 89 | } 90 | return self.response.out.write(template.render(self.template_path('index.html'), for_template)) 91 | 92 | class CheckDomain(BetterHandler): 93 | def render_error(self, url, error='unknown'): 94 | for_template = { 95 | 'title': 'Huh?', 96 | 'domain': url.domain, 97 | } 98 | logging.error("Error on domain '%s': %s", url.domain, error) 99 | return self.response.out.write(template.render(self.template_path('error.html'), for_template)) 100 | 101 | def render_down(self, url): 102 | #downer = Downer(domain=url.domain) 103 | #db.put(downer) 104 | 105 | for_template = { 106 | 'title': "It's not just you!", 107 | 'domain': url.domain, 108 | } 109 | return self.response.out.write(template.render(self.template_path('down.html'), for_template)) 110 | 111 | def render_up(self, url): 112 | for_template = { 113 | 'title': "It's just you.", 114 | 'domain': url.domain, 115 | } 116 | return self.response.out.write(template.render(self.template_path('up.html'), for_template)) 117 | 118 | def render_hurr(self): 119 | for_template = { 120 | 'title': "It's just you.", 121 | } 122 | return self.response.out.write(template.render(self.template_path('hurr.html'), for_template)) 123 | 124 | def get(self, domain): 125 | u = Url(domain) 126 | 127 | if u.missingdomain(): 128 | self.render_error(u, "no domain suffix") 129 | elif u.isself(): 130 | self.render_hurr() 131 | elif u.dos(): 132 | self.render_error(u, "potential DoS") 133 | else: 134 | try: 135 | response = urlfetch.fetch(u.domain, method=urlfetch.HEAD) 136 | except urlfetch.Error: 137 | self.render_down(u) 138 | except urlfetch.InvalidURLError: 139 | self.render_error(u, "urlfetch.InvalidURLError") 140 | except DeadlineExceededError: 141 | self.render_down(u) 142 | else: 143 | if valid_response_code(response.status_code): 144 | self.render_up(u) 145 | else: 146 | self.render_down(u) 147 | 148 | def main(): 149 | application = webapp.WSGIApplication([('/', FrontPage), 150 | (r'/(.*)', CheckDomain)], 151 | debug=False) 152 | wsgiref.handlers.CGIHandler().run(application) 153 | 154 | if __name__ == "__main__": 155 | main() 156 | -------------------------------------------------------------------------------- /gaeunit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | GAEUnit: Google App Engine Unit Test Framework 4 | 5 | Usage: 6 | 7 | 1. Put gaeunit.py into your application directory. Modify 'app.yaml' by 8 | adding the following mapping below the 'handlers:' section: 9 | 10 | - url: /test.* 11 | script: gaeunit.py 12 | 13 | 2. Write your own test cases by extending unittest.TestCase. 14 | 15 | 3. Launch the development web server. To run all tests, point your browser to: 16 | 17 | http://localhost:8080/test (Modify the port if necessary.) 18 | 19 | For plain text output add '?format=plain' to the above URL. 20 | See README.TXT for information on how to run specific tests. 21 | 22 | 4. The results are displayed as the tests are run. 23 | 24 | Visit http://code.google.com/p/gaeunit for more information and updates. 25 | 26 | ------------------------------------------------------------------------------ 27 | Copyright (c) 2008, George Lei and Steven R. Farley. All rights reserved. 28 | 29 | Distributed under the following BSD license: 30 | 31 | Redistribution and use in source and binary forms, with or without 32 | modification, are permitted provided that the following conditions are met: 33 | 34 | * Redistributions of source code must retain the above copyright notice, 35 | this list of conditions and the following disclaimer. 36 | 37 | * Redistributions in binary form must reproduce the above copyright notice, 38 | this list of conditions and the following disclaimer in the documentation 39 | and/or other materials provided with the distribution. 40 | 41 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 42 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 43 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 44 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 45 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 46 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 47 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 48 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 49 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 50 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 51 | ------------------------------------------------------------------------------ 52 | ''' 53 | 54 | __author__ = "George Lei and Steven R. Farley" 55 | __email__ = "George.Z.Lei@Gmail.com" 56 | __version__ = "#Revision: 1.2.2 $"[11:-2] 57 | __copyright__= "Copyright (c) 2008, George Lei and Steven R. Farley" 58 | __license__ = "BSD" 59 | __url__ = "http://code.google.com/p/gaeunit" 60 | 61 | import sys 62 | import os 63 | import unittest 64 | import StringIO 65 | import time 66 | import re 67 | import logging 68 | from google.appengine.ext import webapp 69 | from google.appengine.api import apiproxy_stub_map 70 | from google.appengine.api import datastore_file_stub 71 | from google.appengine.ext.webapp.util import run_wsgi_app 72 | 73 | _DEFAULT_TEST_DIR = 'test' 74 | 75 | 76 | ############################################################################## 77 | # Main request handler 78 | ############################################################################## 79 | 80 | 81 | class MainTestPageHandler(webapp.RequestHandler): 82 | def get(self): 83 | unknown_args = [arg for arg in self.request.arguments() 84 | if arg not in ("format", "package", "name")] 85 | if len(unknown_args) > 0: 86 | errors = [] 87 | for arg in unknown_args: 88 | errors.append(_log_error("The request parameter '%s' is not valid." % arg)) 89 | self.error(404) 90 | self.response.out.write(" ".join(errors)) 91 | return 92 | 93 | format = self.request.get("format", "html") 94 | if format == "html": 95 | self._render_html() 96 | elif format == "plain": 97 | self._render_plain() 98 | else: 99 | error = _log_error("The format '%s' is not valid." % format) 100 | self.error(404) 101 | self.response.out.write(error) 102 | 103 | def _render_html(self): 104 | suite, error = _create_suite(self.request) 105 | if not error: 106 | self.response.out.write(_MAIN_PAGE_CONTENT % _test_suite_to_json(suite)) 107 | else: 108 | self.error(404) 109 | self.response.out.write(error) 110 | 111 | def _render_plain(self): 112 | self.response.headers["Content-Type"] = "text/plain" 113 | runner = unittest.TextTestRunner(self.response.out) 114 | suite, error = _create_suite(self.request) 115 | if not error: 116 | self.response.out.write("====================\n" \ 117 | "GAEUnit Test Results\n" \ 118 | "====================\n\n") 119 | _run_test_suite(runner, suite) 120 | else: 121 | self.error(404) 122 | self.response.out.write(error) 123 | 124 | 125 | ############################################################################## 126 | # JSON test classes 127 | ############################################################################## 128 | 129 | 130 | class JsonTestResult(unittest.TestResult): 131 | def __init__(self): 132 | unittest.TestResult.__init__(self) 133 | self.testNumber = 0 134 | 135 | def render_to(self, stream): 136 | stream.write('{') 137 | stream.write('"runs":"%d", "total":"%d", "errors":"%d", "failures":"%d",' % \ 138 | (self.testsRun, self.testNumber, 139 | len(self.errors), len(self.failures))) 140 | stream.write('"details":') 141 | self._render_errors(stream) 142 | stream.write('}') 143 | 144 | def _render_errors(self, stream): 145 | stream.write('{') 146 | self._render_error_list('errors', self.errors, stream) 147 | stream.write(',') 148 | self._render_error_list('failures', self.failures, stream) 149 | stream.write('}') 150 | 151 | def _render_error_list(self, flavour, errors, stream): 152 | stream.write('"%s":[' % flavour) 153 | for test, err in errors: 154 | stream.write('{"desc":"%s", "detail":"%s"},' % 155 | (self._description(test), self._escape(err))) 156 | if len(errors): 157 | stream.seek(-1, 2) 158 | stream.write("]") 159 | 160 | def _description(self, test): 161 | return test.shortDescription() or str(test) 162 | 163 | def _escape(self, s): 164 | newstr = re.sub('"', '"', s) 165 | newstr = re.sub('\n', '
', newstr) 166 | return newstr 167 | 168 | 169 | class JsonTestRunner: 170 | def run(self, test): 171 | self.result = JsonTestResult() 172 | self.result.testNumber = test.countTestCases() 173 | startTime = time.time() 174 | test(self.result) 175 | stopTime = time.time() 176 | timeTaken = stopTime - startTime 177 | return self.result 178 | 179 | 180 | class JsonTestRunHandler(webapp.RequestHandler): 181 | def get(self): 182 | test_name = self.request.get("name") 183 | _load_default_test_modules() 184 | suite = unittest.defaultTestLoader.loadTestsFromName(test_name) 185 | runner = JsonTestRunner() 186 | _run_test_suite(runner, suite) 187 | runner.result.render_to(self.response.out) 188 | 189 | 190 | # This is not used by the HTML page, but it may be useful for other client test runners. 191 | class JsonTestListHandler(webapp.RequestHandler): 192 | def get(self): 193 | suite, error = _create_suite(self.request) 194 | if not error: 195 | self.response.out.write(_test_suite_to_json(suite)) 196 | else: 197 | self.error(404) 198 | self.response.out.write(error) 199 | 200 | 201 | ############################################################################## 202 | # Module helper functions 203 | ############################################################################## 204 | 205 | 206 | def _create_suite(request): 207 | package_name = request.get("package") 208 | test_name = request.get("name") 209 | 210 | loader = unittest.defaultTestLoader 211 | suite = unittest.TestSuite() 212 | 213 | if not package_name and not test_name: 214 | modules = _load_default_test_modules() 215 | for module in modules: 216 | suite.addTest(loader.loadTestsFromModule(module)) 217 | elif test_name: 218 | try: 219 | _load_default_test_modules() 220 | suite.addTest(loader.loadTestsFromName(test_name)) 221 | except: 222 | pass 223 | elif package_name: 224 | try: 225 | package = reload(__import__(package_name)) 226 | module_names = package.__all__ 227 | for module_name in module_names: 228 | suite.addTest(loader.loadTestsFromName('%s.%s' % (package_name, module_name))) 229 | except: 230 | pass 231 | if suite.countTestCases() == 0: 232 | error = _log_error("'%s' is not found or does not contain any tests." % \ 233 | (test_name or package_name)) 234 | else: 235 | error = None 236 | return (suite, error) 237 | 238 | 239 | def _load_default_test_modules(): 240 | if not _DEFAULT_TEST_DIR in sys.path: 241 | sys.path.append(_DEFAULT_TEST_DIR) 242 | module_names = [mf[0:-3] for mf in os.listdir(_DEFAULT_TEST_DIR) if mf.endswith(".py")] 243 | return [reload(__import__(name)) for name in module_names] 244 | 245 | 246 | def _get_tests_from_suite(suite, tests): 247 | for test in suite: 248 | if isinstance(test, unittest.TestSuite): 249 | _get_tests_from_suite(test, tests) 250 | else: 251 | tests.append(test) 252 | 253 | 254 | def _test_suite_to_json(suite): 255 | tests = [] 256 | _get_tests_from_suite(suite, tests) 257 | test_tuples = [(type(test).__module__, type(test).__name__, test._testMethodName) \ 258 | for test in tests] 259 | test_dict = {} 260 | for test_tuple in test_tuples: 261 | module_name, class_name, method_name = test_tuple 262 | if module_name not in test_dict: 263 | mod_dict = {} 264 | method_list = [] 265 | method_list.append(method_name) 266 | mod_dict[class_name] = method_list 267 | test_dict[module_name] = mod_dict 268 | else: 269 | mod_dict = test_dict[module_name] 270 | if class_name not in mod_dict: 271 | method_list = [] 272 | method_list.append(method_name) 273 | mod_dict[class_name] = method_list 274 | else: 275 | method_list = mod_dict[class_name] 276 | method_list.append(method_name) 277 | 278 | # Python's dictionary and list string representations happen to match JSON formatting. 279 | return str(test_dict) 280 | 281 | 282 | def _run_test_suite(runner, suite): 283 | """Run the test suite. 284 | 285 | Preserve the current development apiproxy, create a new apiproxy and 286 | replace the datastore with a temporary one that will be used for this 287 | test suite, run the test suite, and restore the development apiproxy. 288 | This isolates the test datastore from the development datastore. 289 | 290 | """ 291 | original_apiproxy = apiproxy_stub_map.apiproxy 292 | try: 293 | apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() 294 | temp_stub = datastore_file_stub.DatastoreFileStub('GAEUnitDataStore', None, None) 295 | apiproxy_stub_map.apiproxy.RegisterStub('datastore', temp_stub) 296 | # Allow the other services to be used as-is for tests. 297 | for name in ['user', 'urlfetch', 'mail', 'memcache', 'images']: 298 | apiproxy_stub_map.apiproxy.RegisterStub(name, original_apiproxy.GetStub(name)) 299 | runner.run(suite) 300 | finally: 301 | apiproxy_stub_map.apiproxy = original_apiproxy 302 | 303 | 304 | def _log_error(s): 305 | logging.warn(s) 306 | return s 307 | 308 | 309 | ################################################ 310 | # Browser HTML, CSS, and Javascript 311 | ################################################ 312 | 313 | 314 | # This string uses Python string formatting, so be sure to escape percents as %%. 315 | _MAIN_PAGE_CONTENT = """ 316 | 317 | 318 | 330 | 425 | GAEUnit: Google App Engine Unit Test Framework 426 | 427 | 428 |
429 |
GAEUnit: Google App Engine Unit Test Framework
430 |
Version 1.2.4
431 |
432 |
433 | 434 | 436 | 437 | 438 | 439 | 440 |
435 |
Runs: 0/0Errors: 0Failures: 0
441 |
442 |
443 |
444 |