Web2Py is a full-stack Python web framework that can be compared to Django, but is easier to learn due to convention-over-explicit-statement preference. In this article I'll check how static verification techniques developed by me for many different environments (JSP, Django templates, TAL, ...) can be applied for web2py environment.
Static verification means locating simple bugs without running application thus very high (>95%) testing coverage (and high related cost) is not required. Instead with trying to cover by tests every possible screen/workflow/code line/... we can scan all codebase and search for some constraints. Most of them (based on my experience) are static - do not depend on runtime data thus can be effectively checked without running an application.
Here's short example of web2py template language:
<a href="{{=URL('books','show')}}">...</a>
As you can see web2py will substitute {{=<python-expression>}}s by evaluated result. In this example URL points to existing module controllers/books.py and function inside this module named 'show'. I assume you see the problem here: one can select undefined module / function and it will result in a runtime error.
First example is purely static: refence (URL('books','show')) will not change during runtime, neither the source code. Then our static checker might be applied succesfully: check if all URL's in all *.html files have proper functions defined in source code.
Technical solution can be composed to the following steps:
- locating all resources to check: scanning given directory in filesystem tree
- locating all interesting fragments in HTML file: I used regexp with arguments to easily extract interesting data
- locating functions in *.py code: because controllers are not plain python modules (expects some data in global namespace) I decided just to scan them textually
Another check that can be done is references inside HTML files (to CSS resources, JS files, ...). This also can be automated:
<script type="text/javascript" src="svgcanvas.min.js"></script>
Source code refactorings might break your links/references and static scan might ensure you are not breaking application by refactoring.
Complete source code for URL() / SRC= / HREF= checker:
import sys sys.path.append("web2py/applications") sys.path.append("web2py") import os import copy import re import string from gluon.globals import * from gluon.http import * from gluon.html import * RE_SRC = re.compile(r'src *= *"([^"]*)"') RE_HREF = re.compile(r'href *= *"([^"]*)"') RE_URL = re.compile(r'{{=URL\(\'([^\']*)\'') RE_URL2 = re.compile(r'{{=URL\(\'([^\']*)\' *, *\'([^\']*)\'') FILENAME= None FNR = 0 request = Request() def report_error(s): print "%s:%d: %s" % (FILENAME, FNR, s) def check_src_exists(arg): if arg[0] == "{": # variable value return elif arg[0].find("{{"): # skip this URL pass elif arg[0] == "/": # absolute file fullPath = "web2py/applications" + "/" + arg if not os.path.exists(fullPath): report_error("file %s doesn't exists" % fullPath) else: # relative file fullPath = os.path.dirname(FILENAME) + "/" + arg if not os.path.exists(fullPath): report_error("file %s doesn't exists" % fullPath) def check_href_exists(arg): #print arg if arg.startswith("{{="): pass elif arg.find("{{") > 0: pass elif arg.startswith("/"): if not os.path.exists("web2py/applications" + arg): report_error("absolute file %s doesn't exists" % arg) elif arg.startswith("http://"): # external link, do not check pass elif arg.startswith("https://"): # external link, do not check pass elif arg.startswith("mailto:"): # external link, do not check pass elif arg.startswith("javascript:"): # external link, do not check pass elif arg.find("#") == 0: # anchor, skip pass else: fullPath = os.path.dirname(FILENAME) + "/" + arg if not os.path.exists(fullPath): report_error("relative file %s doesn't exists" % fullPath) def templatePathToPythonPath(templatePath): return string.join(templatePath.replace("/views/", "/controllers/").split("/")[:-1], "/") + ".py" def eq(got, expected): if got != expected: print "got:'%s' != expected:'%s'" % (got, expected) return False return True assert eq(templatePathToPythonPath( "web2py/applications/ad/views/default/details.html"), "web2py/applications/ad/controllers/default.py") assert eq(templatePathToPythonPath( "web2py/applications/ad/views/default/index.html"), "web2py/applications/ad/controllers/default.py") assert eq(templatePathToPythonPath( "web2py/applications/examples/views/ajax_examples/index.html"), "web2py/applications/examples/controllers/ajax_examples.py") name_to_contents = {} def get_file_contents(fileName): global name_to_contents if not name_to_contents.has_key(fileName): if os.path.exists(fileName): f = file(fileName) name_to_contents[fileName] = f.read() f.close() else: report_error("Cannot load %s" % fileName) name_to_contents[fileName] = "" return name_to_contents[fileName] def check_url_exists(url): if FILENAME.find("appadmin.html") > 0: return if url.find(".") > 0: functionName = url.split(".")[-1] else: functionName = url # print "check_url_exists(%s) moduleName=%s" % (url, moduleName) pythonFilePath = templatePathToPythonPath(FILENAME) if get_file_contents(pythonFilePath).find("def " + functionName + "()") < 0: report_error("cannot find %s in %s" % (functionName, pythonFilePath)) def check_url2_exists(moduleName, functionName): if moduleName == "static": return # print "check_url_exists(%s) moduleName=%s" % (url, moduleName) pythonFilePath = string.join(FILENAME.replace("/views/", "/controllers/").split("/")[:-1], "/") + "/" + moduleName + ".py" if get_file_contents(pythonFilePath).find("def " + functionName + "()") < 0: report_error("cannot find %s in %s" % (functionName, pythonFilePath)) def scan_file(path): global FILENAME global FNR FILENAME = path FNR = 0 f = file(path) while 1: FNR += 1 line = f.readline() if not line: break m = RE_SRC.search(line) if m: check_src_exists(m.group(1)) m = RE_HREF.search(line) if m: check_href_exists(m.group(1)) m = RE_URL2.search(line) if m: check_url2_exists(m.group(1), m.group(2)) else: m = RE_URL.search(line) if m: check_url_exists(m.group(1)) f.close() def test_html(directory): for a in os.listdir(directory): if a == "epydoc": continue p = directory + "/" + a if os.path.isdir(p): test_html(p) if a.endswith(".html"): scan_file(p) test_html("web2py/applications")