#!/usr/bin/python -S
#
# Copyright (C) 2009 Andy Chu
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Language-independent tests for json-template.
Uses the testy test framework.
"""
__author__ = 'Andy Chu'
import os
from pprint import pprint
import sys
if __name__ == '__main__':
# for jsontemplate package
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'python'))
from pan.core import cmdapp
from pan.core import params
from pan.core import json
from pan.core import util
from pan.test import testy
import jsontemplate # module under *direct* test
# External verifiers:
from doc import doc_generator
from javascript import verifier as javascript_verifier
from javascript import browser_tests
from java import verifier as java_verifier
from php import verifier as php_verifier
# import *must* be relative to python package to work
import verifier as python_verifier
# This is for making multiline strings look nicer. Strips leading indentation.
B = util.BlockStr
class TrivialTests(testy.Test):
"""For getting the skeleton of code in place!"""
LABELS = ['multilanguage']
def testTrivial(self):
t = testy.ClassDef('Hello')
self.verify.Expansion(t, {}, 'Hello')
class SimpleTests(testy.Test):
"""Basic syntax."""
LABELS = ['multilanguage']
def testConfigurationErrors(self):
self.verify.CompilationError(
jsontemplate.ConfigurationError, '', format_char='!')
self.verify.CompilationError(
jsontemplate.ConfigurationError, '', meta='m')
# Should I assert that meta is not something crazy? Max length of 4 and
# containing {}, [], <> or () is liberal.
def testComment(self):
t = testy.ClassDef('Hello {# Comment} There')
self.verify.Expansion(t, {}, 'Hello There')
def testSpace(self):
t = testy.ClassDef('{.space}{.space}')
self.verify.Expansion(t, {}, ' ')
def testWhitespace(self):
t = testy.ClassDef('{.tab}{.tab}')
self.verify.Expansion(t, {}, '\t\t')
t = testy.ClassDef('Line{.newline}')
self.verify.Expansion(t, {}, 'Line\n')
def testOnlyDeclaration(self):
t = testy.ClassDef('{# Comment}')
self.verify.Expansion(t, {}, '')
class SubstitutionsTest(testy.Test):
"""Language-independent tests for JSON Template."""
LABELS = ['multilanguage']
# TODO: This isn't sed
VERIFIERS = [
python_verifier.InternalTemplateVerifier,
python_verifier.ExternalVerifier]
def testSimpleData(self):
t = testy.ClassDef('Hello {name}, how are you')
self.verify.Expansion(t, {'name': 'Andy'}, 'Hello Andy, how are you')
self.verify.EvaluationError(jsontemplate.UndefinedVariable, t, {})
def testExpandingInteger(self):
t = testy.ClassDef('There are {num} ways to do it')
self.verify.Expansion(t, {'num': 5}, 'There are 5 ways to do it')
# TODO: Implement in Java/PHP
@testy.no_verify('java', 'php')
def testExpandingNull(self):
t = testy.ClassDef('There are {num|str} ways to do it')
self.verify.Expansion(t, {'num': None}, 'There are null ways to do it')
def testVariableFormat(self):
t = testy.ClassDef('Where is your {name|html}')
self.verify.Expansion(t, {'name': '
'}, 'Where is your <head>')
def testDefaultFormatter(self):
t = testy.ClassDef('{name} {val|raw}', default_formatter='html')
self.verify.Expansion(t, {'name': '', 'val': '<>'}, '<head> <>')
def testUndefinedVariable(self):
t = testy.ClassDef('Where is your {name|html}')
self.verify.EvaluationError(jsontemplate.UndefinedVariable, t, {})
# TODO: Implement in Java/PHP
@testy.no_verify('java', 'php')
def testUndefinedVariableUsesUndefinedStr(self):
t = testy.ClassDef('Where is your {name|html}', undefined_str='')
self.verify.Expansion(t, {}, 'Where is your ')
t = testy.ClassDef('Where is your {name|html}', undefined_str='???')
self.verify.Expansion(t, {}, 'Where is your ???')
def testChangingFormattingCharacter(self):
t = testy.ClassDef('Where is your {name:html}', format_char=':')
self.verify.Expansion(t, {'name': ''}, 'Where is your <head>')
def testBadFormatters(self):
self.verify.CompilationError(
jsontemplate.BadFormatter, 'Where is your {name|BAD}')
self.verify.CompilationError(
jsontemplate.BadFormatter, 'Where is your {name|really|bad}')
def testMissingFormatter(self):
self.verify.CompilationError(
jsontemplate.MissingFormatter, 'What is your {name}',
default_formatter=None)
def testEscapeMetacharacter(self):
t = testy.ClassDef('[.meta-left]Hello[.meta-right]', meta='[]')
self.verify.Expansion(t, {}, '[Hello]')
def testMeta(self):
t = testy.ClassDef('Hello {{# Comment}} There', meta='{{}}')
self.verify.Expansion(t, {}, 'Hello There')
def testSubstituteCursor(self):
t = testy.ClassDef('{.section is-new}New since {@} ! {.end}')
self.verify.Expansion(t, {}, '')
self.verify.Expansion(t, {'is-new': 123}, 'New since 123 ! ')
class SectionsTest(testy.PyUnitCompatibleTest):
"""Test sections adn repeated sections."""
LABELS = ['multilanguage']
def testSimpleSection(self):
# Has some newlines too
t = testy.ClassDef(
B("""
{.section is-new}
Hello there
New since {date}!
{date}
{.end}
"""))
self.verify.Expansion(t, {}, '')
d = {'is-new': {'date': 'Monday'}}
self.verify.Expansion(t, d,
B("""
Hello there
New since Monday!
Monday
"""))
def testRepeatedSection(self):
t = testy.ClassDef(
B("""
[header]
---------
[.repeated section people]
[name] [age]
[.end]
"""), meta='[]')
d = {
'header': 'People',
'people': [
{'name': 'Andy', 'age': '20'},
{'name': 'Bob', 'age': '25'},
],
}
self.verify.Expansion(t, d, B("""
People
---------
Andy 20
Bob 25
"""))
# Now test a missing section
self.verify.Expansion(t, {'header': 'Header'}, B("""
Header
---------
"""))
# people=None is the same
self.verify.Expansion(t, {'header': 'Header', 'people': None}, B("""
Header
---------
"""))
def testRepeatedSectionWithDot(self):
t = testy.ClassDef(
B("""
[header]
---------
[.repeated section people]
[greeting] [@]
[.end]
"""), meta='[]')
d = {
'greeting': 'Hello',
'header': 'People',
'people': [
'Andy',
'Bob',
],
}
self.verify.Expansion(t, d, B("""
People
---------
Hello Andy
Hello Bob
"""))
def testNestedRepeatedSections(self):
t = testy.ClassDef(
B("""
[header]
---------
[.repeated section people]
[name]: [.repeated section attributes][@] [.end]
[.end]
"""), meta='[]')
d = {
'header': 'People',
'people': [
{'name': 'Andy', 'attributes': ['jerk', 'cool']},
{'name': 'Bob', 'attributes': ['nice', 'mean', 'fun']},
],
}
self.verify.Expansion(t, d, B("""
People
---------
Andy: jerk cool
Bob: nice mean fun
"""), ignore_all_whitespace=True)
def testRepeatedSectionAtRoot(self):
# This tests expansion of a JSON *list* -- no dictionary in sight
t = testy.ClassDef('[.repeated section @][@] [.end]', meta='[]')
self.verify.Expansion(t, ['Andy', 'Bob'], 'Andy Bob ')
def testAlternatesWith(self):
t = testy.ClassDef(
B("""
[header]
---------
[.repeated section people]
Name: [name] Age: [age]
[.alternates with]
*****
[.end]
"""), meta='[]')
d = {
'header': 'People',
'people': [
{'name': 'Andy', 'age': '20'},
{'name': 'Bob', 'age': '25'},
{'name': 'Carol', 'age': '30'},
],
}
self.verify.Expansion(t, d, B("""
People
---------
Name: Andy Age: 20
*****
Name: Bob Age: 25
*****
Name: Carol Age: 30
"""))
def testSection(self):
t = testy.ClassDef(
B("""
[header]
---------
[.section people]
There are [summary] here:
[.repeated section entries]
[name] [age]
[.end]
[footer]
[.end]
"""), meta='[]')
d = {
'header': 'People',
'people': {
'summary': '2 dudes',
'footer': 'Footer',
'entries': [
{'name': 'Andy', 'age': '20'},
{'name': 'Bob', 'age': '25'},
],
}
}
# Relies on smart-indent behavior; ignore whitespace for some
# implementations
self.verify.Expansion(t, d, B("""
People
---------
There are 2 dudes here:
Andy 20
Bob 25
Footer
"""), ignore_all_whitespace=True)
self.verify.Expansion(t, {'header': 'People'}, B("""
People
---------
"""))
# Now test with people=None. This is the same as omitting the key.
self.verify.Expansion(t, {'header': 'People', 'people': None}, B("""
People
---------
"""))
def testExpansionInInnerScope(self):
t = testy.ClassDef(
B("""
[url]
[.section person]
[name] [age] [url]
[.end]
"""), meta='[]')
d = {
'url': 'http://example.com',
'person': {
'name': 'Andy',
'age': 30,
}
}
self.verify.Expansion(t, d, B("""
http://example.com
Andy 30 http://example.com
"""))
d = {
'person': {
'name': 'Andy',
'age': 30,
}
}
self.verify.EvaluationError(jsontemplate.UndefinedVariable, t, d)
def testTooManyEndBlocks(self):
self.verify.CompilationError(jsontemplate.TemplateSyntaxError, B("""
{.section people}
{.end}
{.end}
"""))
def testTooFewEndBlocks(self):
self.verify.CompilationError(jsontemplate.TemplateSyntaxError, B("""
{.section people}
{.section cars}
{.end}
"""))
def testSectionAndRepeatedSection(self):
"""A repeated section within a section."""
t = testy.ClassDef(
B("""
[header]
---------
[.section people]
[.repeated section @]
| [name] |
[age] |
[.end]
[.end]
"""), meta='[]')
d = {
'header': 'People',
'people': [
{'name': 'Andy', 'age': '20'},
{'name': 'Bob', 'age': '25'},
],
}
expected = B("""
People
---------
""")
self.verify.Expansion(t, d, expected, ignore_whitespace=True)
self.verify.Expansion(t, {'header': 'People'}, B("""
People
---------
"""))
def testBadContext(self):
# Note: A list isn't really a valid top level context, but this case should
# be some kind of error.
t = testy.ClassDef("{foo}")
self.verify.EvaluationError(jsontemplate.UndefinedVariable, t, [])
def testSectionOr(self):
t = testy.ClassDef(
B("""
Hello there.
{.section person}
{name} {age} {url}
{.or}
No person.
{.end}
{url}
"""))
d = {
'url': 'http://example.com',
'person': {
'name': 'Andy',
'age': 30,
}
}
expected = B("""
Hello there.
Andy 30 http://example.com
http://example.com
""")
self.verify.Expansion(t, d, expected)
d = { 'url': 'http://example.com' }
expected = B("""
Hello there.
No person.
http://example.com
""")
self.verify.Expansion(t, d, expected)
@testy.labels('documentation')
def testRepeatedSectionOr(self):
t = testy.ClassDef(
B("""
{header}
---------
{.repeated section people}
{name} {age}
{.or}
No people.
{.end}
"""))
with_people = {
'header': 'People',
'people': [
{'name': 'Andy', 'age': '20'},
{'name': 'Bob', 'age': '25'},
],
}
self.verify.Expansion(t, with_people, B("""
People
---------
Andy 20
Bob 25
"""))
# Empty list
without_people = {
'header': 'People',
'people': [],
}
self.verify.Expansion(t, without_people, B("""
People
---------
No people.
"""))
# Null
d = {
'header': 'People',
'people': None,
}
self.verify.Expansion(t, d, B("""
People
---------
No people.
"""))
# Now there are 3 clauses
t = testy.ClassDef(
B("""
{header}
---------
{.repeated section people}
{name} {age}
{.alternates with}
--
{.or}
No people.
{.end}
"""))
self.verify.Expansion(t, with_people, B("""
People
---------
Andy 20
--
Bob 25
"""))
# Empty list
d = {
'header': 'People',
'people': [],
}
self.verify.Expansion(t, without_people, B("""
People
---------
No people.
"""))
def testEmptyListWithSection(self):
# From the wiki
t = testy.ClassDef(
B("""
{.section title-results}
Results.
{.repeated section @}
{num}. {line}
{.end}
{.end}
"""))
d = {
'title-results': [
{'num': 1, 'line': 'one'},
{'num': 2, 'line': 'two'},
]
}
self.verify.Expansion(t, d, B("""
Results.
1. one
2. two
"""), ignore_all_whitespace=True)
d = { 'title-results': [] }
self.verify.Expansion(t, d, '')
class DottedLookupTest(testy.Test):
"""Test substitutions like {foo.bar.baz}."""
LABELS = ['multilanguage']
@testy.no_verify('java', 'php')
def testDottedLookup(self):
t = testy.ClassDef('{foo.bar}')
self.verify.Expansion(
t,
{'foo': {'bar': 'Hello'}},
'Hello')
@testy.no_verify('java', 'php')
def testDottedLookupErrors(self):
# TODO: Also test everything with setting undefined_str
t = testy.ClassDef('{foo.bar}')
# The second lookup doesn't look up the stack to find 'bar'
self.verify.EvaluationError(
jsontemplate.UndefinedVariable,
t,
{'foo': {}, 'bar': 100})
# Can't look up bar in 100
self.verify.EvaluationError(
jsontemplate.UndefinedVariable,
t,
{'foo': 100})
# Can't look up bar in a list
self.verify.EvaluationError(
jsontemplate.UndefinedVariable,
t,
{'foo': []})
self.verify.EvaluationError(
jsontemplate.UndefinedVariable,
t,
{'foo': {}})
self.verify.EvaluationError(
jsontemplate.UndefinedVariable,
t,
{})
@testy.no_verify('java', 'php')
def testDottedLookupErrorsWithUndefinedStr(self):
t = testy.ClassDef('{foo.bar}', undefined_str='UNDEFINED')
# The second lookup doesn't look up the stack to find 'bar'
self.verify.Expansion(
t,
{'foo': {}, 'bar': 100},
'UNDEFINED')
@testy.no_verify('java', 'php')
def testThreeLookups(self):
t = testy.ClassDef('{foo.bar.baz}')
self.verify.Expansion(
t,
{'foo': {'bar': {'baz': 'Hello'}}},
'Hello')
self.verify.EvaluationError(
jsontemplate.UndefinedVariable,
t,
{'foo': 100})
@testy.no_verify('java', 'php')
def testScopedLookup(self):
t = testy.ClassDef(
B("""
{.section foo}
{bar.baz}
{.end}
"""))
self.verify.Expansion(
t,
{'foo': {'bar': {'baz': 'Hello'}}},
' Hello\n')
# We should find 'bar' even if it's not under foo
self.verify.Expansion(
t,
{ 'foo': {'unused': 1},
'bar': {'baz': 'Hello'},
},
' Hello\n')
self.verify.EvaluationError(
jsontemplate.UndefinedVariable,
t,
{'foo': 100})
class SpecialVariableTest(testy.Test):
"""Tests the special $index variable."""
LABELS = ['multilanguage']
@testy.no_verify('javascript', 'java', 'php')
def testIndex(self):
t = testy.ClassDef(
B("""
{.repeated section @}
{$index} {name}
{.end}
"""))
data = [
{'name': 'Spam'},
{'name': 'Eggs'},
]
expected = B("""
0 Spam
1 Eggs
""")
self.verify.Expansion(t, data, expected)
@testy.no_verify('javascript', 'java', 'php')
def testTwoIndices(self):
t = testy.ClassDef(
B("""
{.repeated section albums}
{$index} {name}
{.repeated section songs}
{$index} {@}
{.end}
{.end}
"""))
data = {
'albums': [
{ 'name': 'Diary of a Madman',
'songs': ['Over the Mountain', 'S.A.T.O']},
{ 'name': 'Bark at the Moon',
'songs': ['Waiting for Darkness']},
]
}
expected = B("""
0 Diary of a Madman
0 Over the Mountain
1 S.A.T.O
1 Bark at the Moon
0 Waiting for Darkness
""")
self.verify.Expansion(t, data, expected)
@testy.no_verify('javascript', 'java', 'php')
def testUndefinedIndex(self):
t = testy.ClassDef(
B("""
{.section foo}
{$index} {name}
{.end}
"""))
data = {'foo': 'bar'}
self.verify.EvaluationError(jsontemplate.UndefinedVariable, t, data)
class StandardFormattersTest(testy.Test):
"""Test that each implementation implements the standard formatters."""
LABELS = ['multilanguage']
def testHtmlFormatter(self):
t = testy.ClassDef('{name|html}')
self.verify.Expansion(
t, {'name': '""'}, '"<tag>"')
def testHtmlAttrValueFormatter(self):
t = testy.ClassDef('')
self.verify.Expansion(
t, {'url': '"<>&'}, '')
class AllFormattersTest(testy.Test):
"""Test that each implementation implements the standard formatters."""
LABELS = ['multilanguage']
@testy.only_verify('python')
def testPrintfFormatter(self):
t = testy.ClassDef('{num|printf %.3f}')
self.verify.ExpansionWithAllFormatters(
t, {'num': 1.0/3}, '0.333')
class DocumentationTest(testy.Test):
"""Test cases added for the sake of documentation."""
# TODO: The default labels for this test should be 'documentation'
LABELS = ['multilanguage']
@testy.labels('documentation')
def testSearchResultsExample(self):
# TODO: Come up with a better search results example
return
@testy.labels('documentation', 'live-js', 'blog-format')
def testTableExample(self):
t = testy.ClassDef("""\
{# This is a comment and will be removed from the output.}
{.section songs}
Songs in '{playlist-name}'
{.repeated section @}
| Play
| {title} |
{artist} |
{.end}
{.or}
(No page content matches)
{.end}
""")
d = {
"playlist-name": "Epic Playlist",
"url-base": "http://example.com/music/",
"songs": [
{ "title": "Sounds Like Thunder",
"artist": "Grayceon",
"url": "1.mp3"
},
{ "title": "Their Hooves Carve Craters in the Earth",
"artist": "Thou",
"url": "2.mp3"
}
]
}
expected = """\
Songs in 'Epic Playlist'
| Play
| Sounds Like Thunder |
Grayceon |
| Play
| Their Hooves Carve Craters in the Earth |
Thou |
"""
self.verify.Expansion(t, d, expected, ignore_all_whitespace=True)
def main(argv):
this_dir = os.path.dirname(__file__)
# e.g. this works on my Ubuntu system
default_v8_shell = os.path.join(
this_dir, 'javascript', 'v8shell', 'linux-i686', 'shell')
default_java = os.path.join(
os.getenv('JAVA_HOME', ''), 'bin', 'java')
default_php = os.path.join('/','usr', 'bin', 'php')
run_params = testy.TEST_RUN_PARAMS + [
params.OptionalString(
'v8-shell', default=default_v8_shell,
help='Location of the v8 shell to run JavaScript tests'),
params.OptionalString(
'java-launcher', default=default_java,
help='Location of the Java launcher to run Java tests'),
params.OptionalString(
'php-launcher', default=default_php,
help='Location of the PHP launcher to run PHP tests'),
# Until we have better test filtering:
params.OptionalBoolean('python', help='Run Python tests'),
params.OptionalBoolean('java', help='Run Java tests'),
params.OptionalBoolean('php', help='Run PHP tests'),
params.OptionalBoolean('javascript', help='Run JavaScript tests'),
params.OptionalString(
'browser-test-out-dir', shortcut='b',
help='Write browser tests to this directory'),
params.OptionalString(
'doc-output-dir', shortcut='d',
help='Write generated docs to this directory'),
params.OptionalBoolean('all-tests', help='Run all tests'),
]
options = cmdapp.ParseArgv(argv, run_params)
int_py_verifier = python_verifier.InternalTemplateVerifier()
python_impl = os.path.join(this_dir, 'python', 'expand.py')
py_verifier = python_verifier.ExternalVerifier(python_impl)
js_impl = os.path.join(this_dir, 'javascript', 'json-template.js')
helpers = os.path.join(this_dir, 'pan', 'javascript', 'test_helpers.js')
if sys.platform == 'win32':
js_verifier = javascript_verifier.CScriptVerifier(js_impl, helpers)
else:
js_verifier = javascript_verifier.V8ShellVerifier(
options.v8_shell, js_impl, helpers)
java_impl = os.path.join(this_dir, 'java', 'jsontemplate.jar')
java_test_classes = os.path.join(this_dir, 'java', 'jsontemplate_test.jar')
jv_verifier = java_verifier.JavaVerifier(
options.java_launcher, java_impl, java_test_classes)
php_impl = os.path.join(this_dir, 'php', 'jsontemplate.php')
ph_verifier = php_verifier.PhpVerifier(
options.php_launcher, php_impl)
filt = testy.MakeTestClassFilter(
label='multilanguage', regex=options.test_regex)
multi_tests = testy.GetTestClasses(__import__(__name__), filt)
internal_tests = [m(int_py_verifier) for m in multi_tests]
# External versions
if options.all_tests:
tests = internal_tests
tests.extend(m(py_verifier) for m in multi_tests)
tests.extend(m(js_verifier) for m in multi_tests)
tests.extend(m(jv_verifier) for m in multi_tests)
tests.extend(m(ph_verifier) for m in multi_tests)
elif options.python:
tests = [m(py_verifier) for m in multi_tests]
elif options.javascript:
tests = [m(js_verifier) for m in multi_tests]
elif options.java:
tests = [m(jv_verifier) for m in multi_tests]
elif options.php:
tests = [m(ph_verifier) for m in multi_tests]
elif options.doc_output_dir:
# Generates the HTML fragments.
docgen = doc_generator.DocGenerator(options.doc_output_dir)
# Run the internal tests before generating docs.
tests = []
tests.extend(m(int_py_verifier) for m in multi_tests)
tests.extend(m(docgen) for m in multi_tests)
tests.extend([
DocumentationTest(int_py_verifier), DocumentationTest(docgen),
])
elif options.browser_test_out_dir:
testgen = browser_tests.TestGenerator()
# Run the internal tests before generating browser tests.
tests = []
tests.extend(m(int_py_verifier) for m in multi_tests)
tests.extend(m(testgen) for m in multi_tests)
else:
tests = internal_tests
testy.RunTests(tests, options)
# Write HTML *after* running all tests
if options.browser_test_out_dir:
testgen.WriteHtml(options.browser_test_out_dir)
if __name__ == '__main__':
main(sys.argv[1:])