├── .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 |
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 |
432 |
433 |
434 | |
435 |
436 | | Runs: 0/0 |
437 | Errors: 0 |
438 | Failures: 0 |
439 |
440 |
441 |
442 |
443 |