diff --git a/validator/build.py b/validator/build.py
index 5ac5c7a5b38e..6a8c8c7ec5cf 100755
--- a/validator/build.py
+++ b/validator/build.py
@@ -17,6 +17,7 @@
"""A build script which (thus far) works on Ubuntu 14."""
+import logging
import os
import platform
import re
@@ -36,6 +37,7 @@ def Die(msg):
def CheckPrereqs():
"""Checks that various prerequisites for this script are satisfied."""
+ logging.info('entering ...')
if platform.system() != 'Linux':
Die('Sorry, this script assumes Linux thus far, e.g. Ubuntu 14. '
@@ -43,7 +45,8 @@ def CheckPrereqs():
# Ensure source files are available.
for f in ['validator.protoascii', 'validator.proto', 'validator_gen.py',
- 'package.json']:
+ 'package.json', 'validator.js', 'validator_test.js',
+ 'validator-in-browser.js', 'tokenize-css.js', 'parse-css.js']:
if not os.path.exists(f):
Die('%s not found. Must run in amp_validator source directory.' % f)
@@ -83,6 +86,7 @@ def CheckPrereqs():
subprocess.check_output(['java', '-version'], stderr=subprocess.STDOUT)
except:
Die('Java missing. Try "apt-get install openjdk-7-jre"')
+ logging.info('... done')
def SetupOutDir(out_dir):
@@ -92,17 +96,21 @@ def SetupOutDir(out_dir):
out_dir: directory name of the output directory. Must not have slashes,
dots, etc.
"""
+ logging.info('entering ...')
assert re.match(r'^[a-zA-Z_\-0-9]+$', out_dir), 'bad out_dir: %s' % out_dir
if os.path.exists(out_dir):
subprocess.check_call(['rm', '-rf', out_dir])
os.mkdir(out_dir)
+ logging.info('... done')
def InstallNodeDependencies():
+ logging.info('entering ...')
# Install the project dependencies specified in package.json into
# node_modules.
subprocess.check_call(['npm', 'install'])
+ logging.info('... done')
def GenValidatorPb2Py(out_dir):
@@ -112,11 +120,13 @@ def GenValidatorPb2Py(out_dir):
out_dir: directory name of the output directory. Must not have slashes,
dots, etc.
"""
+ logging.info('entering ...')
assert re.match(r'^[a-zA-Z_\-0-9]+$', out_dir), 'bad out_dir: %s' % out_dir
subprocess.check_call(['protoc', 'validator.proto',
'--python_out=%s' % out_dir])
- open('codegen/__init__.py', 'w').close()
+ open('%s/__init__.py' % out_dir, 'w').close()
+ logging.info('... done')
def GenValidatorGeneratedJs(out_dir):
@@ -126,6 +136,7 @@ def GenValidatorGeneratedJs(out_dir):
out_dir: directory name of the output directory. Must not have slashes,
dots, etc.
"""
+ logging.info('entering ...')
assert re.match(r'^[a-zA-Z_\-0-9]+$', out_dir), 'bad out_dir: %s' % out_dir
# These imports happen late, within this method because they don't necessarily
@@ -142,34 +153,40 @@ def GenValidatorGeneratedJs(out_dir):
descriptor=descriptor,
out=out)
out.append('')
- f = open('codegen/validator-generated.js', 'w')
+ f = open('%s/validator-generated.js' % out_dir, 'w')
f.write('\n'.join(out))
f.close()
+ logging.info('... done')
+
+
+def CompileWithClosure(js_files, closure_entry_points, output_file):
+ cmd = ['java', '-jar', 'node_modules/google-closure-compiler/compiler.jar',
+ '--language_in=ECMASCRIPT6_STRICT', '--language_out=ES5_STRICT',
+ '--js_output_file=%s' % output_file,
+ '--only_closure_dependencies']
+ cmd += ['--closure_entry_point=%s' % e for e in closure_entry_points]
+ cmd += ['node_modules/google-closure-library/closure/**.js',
+ '!node_modules/google-closure-library/closure/**_test.js',
+ 'node_modules/google-closure-library/third_party/closure/**.js',
+ '!node_modules/google-closure-library/third_party/closure/**_test.js']
+ cmd += js_files
+ subprocess.check_call(cmd)
def CompileValidatorMinified(out_dir):
- GOOG = 'node_modules/google-closure-library/closure/goog/'
- subprocess.check_call([
- 'node_modules/google-closure-library/closure/bin/build/closurebuilder.py',
- '--output_mode=compiled',
- '--compiler_jar=node_modules/google-closure-compiler/compiler.jar',
- '--root=node_modules/google-closure-library/closure',
- '--root=node_modules/google-closure-library/third_party/closure',
- '--output_file=codegen/validator_minified.js',
- '--input=codegen/validator-generated.js',
- '--input=validator-in-browser.js',
- '--input=validator.js',
- '--compiler_flags=--language_in=ECMASCRIPT6_STRICT',
- '--compiler_flags=--language_out=ES5_STRICT',
- 'htmlparser.js',
- 'parse-css.js',
- 'tokenize-css.js',
- 'codegen/validator-generated.js',
- 'validator-in-browser.js',
- 'validator.js'])
+ logging.info('entering ...')
+ CompileWithClosure(
+ js_files=['htmlparser.js', 'parse-css.js', 'tokenize-css.js',
+ '%s/validator-generated.js' % out_dir,
+ 'validator-in-browser.js', 'validator.js'],
+ closure_entry_points=['amp.validator.validateString',
+ 'amp.validator.renderValidationResult'],
+ output_file='%s/validator_minified.js' % out_dir)
+ logging.info('... done')
def GenerateValidateBin(out_dir):
+ logging.info('entering ...')
f = open('%s/validate' % out_dir, 'w')
f.write('#!/usr/bin/nodejs\n')
for l in open('%s/validator_minified.js' % out_dir):
@@ -208,11 +225,14 @@ def GenerateValidateBin(out_dir):
}
""")
os.chmod('%s/validate' % out_dir, 0750)
+ logging.info('... done')
def RunSmokeTest(out_dir):
+ logging.info('entering ...')
# Run codegen/validate on the minimum valid amp and observe that it passes.
- p = subprocess.Popen(['codegen/validate', 'testdata/minimum_valid_amp.html'],
+ p = subprocess.Popen(['%s/validate' % out_dir,
+ 'testdata/feature_tests/minimum_valid_amp.html'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = p.communicate()
if ('PASS\n', '', p.returncode) != (stdout, stderr, 0):
@@ -221,15 +241,56 @@ def RunSmokeTest(out_dir):
# Run codegen/validate on an empty file and observe that it fails.
open('%s/empty.html' % out_dir, 'w').close()
- p = subprocess.Popen(['codegen/validate', '%s/empty.html' % out_dir],
+ p = subprocess.Popen(['%s/validate' % out_dir, '%s/empty.html' % out_dir],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = p.communicate()
if p.returncode != 1:
Die('smoke test failed. Expected p.returncode==1, saw: %s' % p.returncode)
if not stderr.startswith('FAIL\nempty.html:1:0 MANDATORY_TAG_MISSING'):
Die('smoke test failed; stderr was: "%s"' % stdout)
+ logging.info('... done')
+
+
+def CompileValidatorTestMinified(out_dir):
+ logging.info('entering ...')
+ CompileWithClosure(
+ js_files=['htmlparser.js', 'parse-css.js', 'tokenize-css.js',
+ '%s/validator-generated.js' % out_dir,
+ 'validator-in-browser.js', 'validator.js', 'validator_test.js'],
+ closure_entry_points=['amp.validator.validatorTest'],
+ output_file='%s/validator_test_minified.js' % out_dir)
+ logging.info('... success')
+
+
+def GenerateValidatorTest(out_dir):
+ logging.info('entering ...')
+ f = open('%s/validator_test' % out_dir, 'w')
+ f.write('#!/usr/bin/nodejs\n')
+ for l in open('%s/validator_test_minified.js' % out_dir):
+ f.write(l)
+ f.write("""
+ var assert = require('assert');
+ var fs = require('fs');
+ var path = require('path');
+ var JasmineRunner = require('jasmine');
+ var jasmine = new JasmineRunner();
+ amp.validator.validatorTest(['testdata'], assert, fs, path, describe, it);
+ jasmine.onComplete(function (passed) { process.exit(passed ? 0 : 1); });
+ jasmine.execute();
+ """)
+ os.chmod('%s/validator_test' % out_dir, 0750)
+ logging.info('... success')
+
+
+def RunValidatorTest(out_dir):
+ logging.info('entering ...')
+ # Run codegen/validate on the minimum valid amp and observe that it passes.
+ subprocess.check_call(['%s/validator_test' % out_dir])
+ logging.info('... success')
+logging.basicConfig(format='[[%(filename)s %(funcName)s]] - %(message)s',
+ level=logging.INFO)
CheckPrereqs()
InstallNodeDependencies()
SetupOutDir(out_dir='codegen')
@@ -238,4 +299,6 @@ def RunSmokeTest(out_dir):
CompileValidatorMinified(out_dir='codegen')
GenerateValidateBin(out_dir='codegen')
RunSmokeTest(out_dir='codegen')
-print 'Success - codegen/validate built and tested.'
+CompileValidatorTestMinified(out_dir='codegen')
+GenerateValidatorTest(out_dir='codegen')
+RunValidatorTest(out_dir='codegen')
diff --git a/validator/package.json b/validator/package.json
index 1c7c62af79d3..b3b855cdf710 100644
--- a/validator/package.json
+++ b/validator/package.json
@@ -14,6 +14,7 @@
"dependencies": {},
"devDependencies": {
"google-closure-compiler": "20151015.0.0",
- "google-closure-library": "20151015.0.0"
+ "google-closure-library": "20151015.0.0",
+ "jasmine": "2.3.2"
}
}
diff --git a/validator/testdata/feature_tests/amp_font.html b/validator/testdata/feature_tests/amp_font.html
new file mode 100644
index 000000000000..68c266ea01ea
--- /dev/null
+++ b/validator/testdata/feature_tests/amp_font.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/validator/testdata/feature_tests/amp_font.out b/validator/testdata/feature_tests/amp_font.out
new file mode 100644
index 000000000000..7ef22e9a431a
--- /dev/null
+++ b/validator/testdata/feature_tests/amp_font.out
@@ -0,0 +1 @@
+PASS
diff --git a/validator/testdata/feature_tests/bad_viewport.html b/validator/testdata/feature_tests/bad_viewport.html
new file mode 100644
index 000000000000..eb6978c65ff6
--- /dev/null
+++ b/validator/testdata/feature_tests/bad_viewport.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+Hello, world.
+
+
diff --git a/validator/testdata/feature_tests/bad_viewport.out b/validator/testdata/feature_tests/bad_viewport.out
new file mode 100644
index 000000000000..c664564808a7
--- /dev/null
+++ b/validator/testdata/feature_tests/bad_viewport.out
@@ -0,0 +1,5 @@
+FAIL
+feature_tests/bad_viewport.html:25:2 DISALLOWED_PROPERTY_IN_ATTR_VALUE content="...foo=..." (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt)
+feature_tests/bad_viewport.html:25:2 INVALID_PROPERTY_VALUE_IN_ATTR_VALUE content="...minimum-scale=not-a-number..." (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt)
+feature_tests/bad_viewport.html:25:2 MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE content="...width=..." (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt)
+feature_tests/bad_viewport.html:32:7 MANDATORY_TAG_MISSING viewport declaration (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt)
diff --git a/validator/testdata/feature_tests/css_length.html b/validator/testdata/feature_tests/css_length.html
new file mode 100644
index 000000000000..f5e4affec227
--- /dev/null
+++ b/validator/testdata/feature_tests/css_length.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Hello, world.
+
+
diff --git a/validator/testdata/feature_tests/css_length.out b/validator/testdata/feature_tests/css_length.out
new file mode 100644
index 000000000000..7ef22e9a431a
--- /dev/null
+++ b/validator/testdata/feature_tests/css_length.out
@@ -0,0 +1 @@
+PASS
diff --git a/validator/testdata/feature_tests/dog_doc_type.html b/validator/testdata/feature_tests/dog_doc_type.html
new file mode 100644
index 000000000000..6448431872c5
--- /dev/null
+++ b/validator/testdata/feature_tests/dog_doc_type.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+Hello this is dog.
+
+
diff --git a/validator/testdata/feature_tests/dog_doc_type.out b/validator/testdata/feature_tests/dog_doc_type.out
new file mode 100644
index 000000000000..710eeed32516
--- /dev/null
+++ b/validator/testdata/feature_tests/dog_doc_type.out
@@ -0,0 +1,3 @@
+FAIL
+feature_tests/dog_doc_type.html:23:0 DISALLOWED_ATTR 🐶
+feature_tests/dog_doc_type.html:35:7 MANDATORY_TAG_MISSING html ⚡ for top-level html (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd)
\ No newline at end of file
diff --git a/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.html b/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.html
new file mode 100644
index 000000000000..fd8b9eea7ba3
--- /dev/null
+++ b/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.out b/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.out
new file mode 100644
index 000000000000..f869314aa486
--- /dev/null
+++ b/validator/testdata/feature_tests/duplicate_unique_tags_and_wrong_parents.out
@@ -0,0 +1,4 @@
+FAIL
+feature_tests/duplicate_unique_tags_and_wrong_parents.html:31:2 DUPLICATE_UNIQUE_TAG author stylesheet (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#stylesheets)
+feature_tests/duplicate_unique_tags_and_wrong_parents.html:35:0 DISALLOWED_ATTR amp-custom (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#opacity)
+feature_tests/duplicate_unique_tags_and_wrong_parents.html:35:0 WRONG_PARENT_TAG body > style (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#opacity)
diff --git a/validator/testdata/feature_tests/empty.html b/validator/testdata/feature_tests/empty.html
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/validator/testdata/feature_tests/empty.out b/validator/testdata/feature_tests/empty.out
new file mode 100644
index 000000000000..c07dfdcc9675
--- /dev/null
+++ b/validator/testdata/feature_tests/empty.out
@@ -0,0 +1,10 @@
+FAIL
+feature_tests/empty.html:1:0 MANDATORY_TAG_MISSING html ⚡ for top-level html (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd)
+feature_tests/empty.html:1:0 MANDATORY_TAG_MISSING head (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#crps)
+feature_tests/empty.html:1:0 MANDATORY_TAG_MISSING charset utf-8 declaration (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#chrs)
+feature_tests/empty.html:1:0 MANDATORY_TAG_MISSING viewport declaration (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt)
+feature_tests/empty.html:1:0 MANDATORY_TAG_MISSING mandatory style (js enabled) opacity 0 (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#opacity)
+feature_tests/empty.html:1:0 MANDATORY_TAG_MISSING mandatory style (noscript) opacity 1 (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#opacity)
+feature_tests/empty.html:1:0 MANDATORY_TAG_MISSING noscript enclosure for mandatory style (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#opacity)
+feature_tests/empty.html:1:0 MANDATORY_TAG_MISSING body (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#crps)
+feature_tests/empty.html:1:0 MANDATORY_TAG_MISSING amphtml engine v1.js script (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#scrpt)
diff --git a/validator/testdata/feature_tests/empty_stylesheet.html b/validator/testdata/feature_tests/empty_stylesheet.html
new file mode 100644
index 000000000000..9782f2355a9d
--- /dev/null
+++ b/validator/testdata/feature_tests/empty_stylesheet.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Hello, world.
+
+
diff --git a/validator/testdata/feature_tests/empty_stylesheet.out b/validator/testdata/feature_tests/empty_stylesheet.out
new file mode 100644
index 000000000000..7ef22e9a431a
--- /dev/null
+++ b/validator/testdata/feature_tests/empty_stylesheet.out
@@ -0,0 +1 @@
+PASS
diff --git a/validator/testdata/feature_tests/incorrect_mandatory_style.html b/validator/testdata/feature_tests/incorrect_mandatory_style.html
new file mode 100644
index 000000000000..274839531c6b
--- /dev/null
+++ b/validator/testdata/feature_tests/incorrect_mandatory_style.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/validator/testdata/feature_tests/incorrect_mandatory_style.out b/validator/testdata/feature_tests/incorrect_mandatory_style.out
new file mode 100644
index 000000000000..62206679f58d
--- /dev/null
+++ b/validator/testdata/feature_tests/incorrect_mandatory_style.out
@@ -0,0 +1,3 @@
+FAIL
+feature_tests/incorrect_mandatory_style.html:28:2 MANDATORY_CDATA_MISSING_OR_INCORRECT mandatory style (js enabled) opacity 0 (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#opacity)
+feature_tests/incorrect_mandatory_style.html:28:74 MANDATORY_CDATA_MISSING_OR_INCORRECT mandatory style (noscript) opacity 1 (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#opacity)
diff --git a/validator/testdata/feature_tests/lang_attr.html b/validator/testdata/feature_tests/lang_attr.html
new file mode 100644
index 000000000000..40262903cf48
--- /dev/null
+++ b/validator/testdata/feature_tests/lang_attr.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+Hello, world.
+
+
diff --git a/validator/testdata/feature_tests/lang_attr.out b/validator/testdata/feature_tests/lang_attr.out
new file mode 100644
index 000000000000..7ef22e9a431a
--- /dev/null
+++ b/validator/testdata/feature_tests/lang_attr.out
@@ -0,0 +1 @@
+PASS
diff --git a/validator/testdata/feature_tests/link_meta_values.html b/validator/testdata/feature_tests/link_meta_values.html
new file mode 100644
index 000000000000..df18efe5bf2c
--- /dev/null
+++ b/validator/testdata/feature_tests/link_meta_values.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Hello, world.
+
+
diff --git a/validator/testdata/feature_tests/link_meta_values.out b/validator/testdata/feature_tests/link_meta_values.out
new file mode 100644
index 000000000000..1929a2e1893c
--- /dev/null
+++ b/validator/testdata/feature_tests/link_meta_values.out
@@ -0,0 +1,3 @@
+FAIL
+feature_tests/link_meta_values.html:32:2 INVALID_ATTR_VALUE name=content-disposition (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt)
+feature_tests/link_meta_values.html:33:2 INVALID_ATTR_VALUE rel=unknown (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#canon)
diff --git a/validator/testdata/feature_tests/mandatory_dimensions.html b/validator/testdata/feature_tests/mandatory_dimensions.html
new file mode 100644
index 000000000000..16ddbda9300a
--- /dev/null
+++ b/validator/testdata/feature_tests/mandatory_dimensions.html
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/validator/testdata/feature_tests/mandatory_dimensions.out b/validator/testdata/feature_tests/mandatory_dimensions.out
new file mode 100644
index 000000000000..639e8059c0ac
--- /dev/null
+++ b/validator/testdata/feature_tests/mandatory_dimensions.out
@@ -0,0 +1,20 @@
+FAIL
+feature_tests/mandatory_dimensions.html:102:4 MANDATORY_ATTR_MISSING width (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
+feature_tests/mandatory_dimensions.html:102:4 MANDATORY_ATTR_MISSING height (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
+feature_tests/mandatory_dimensions.html:103:4 MANDATORY_ATTR_MISSING width (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
+feature_tests/mandatory_dimensions.html:103:4 MANDATORY_ATTR_MISSING height (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
+feature_tests/mandatory_dimensions.html:104:4 MANDATORY_ATTR_MISSING height (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
+feature_tests/mandatory_dimensions.html:105:4 MANDATORY_ATTR_MISSING width (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
+feature_tests/mandatory_dimensions.html:108:4 MANDATORY_ATTR_MISSING src (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
+feature_tests/mandatory_dimensions.html:109:4 MANDATORY_ATTR_MISSING src (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-anim/amp-anim.md)
+feature_tests/mandatory_dimensions.html:112:4 DISALLOWED_ATTR srcset (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-audio/amp-audio.md)
+feature_tests/mandatory_dimensions.html:113:4 DISALLOWED_ATTR srcset (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-ad.md)
+feature_tests/mandatory_dimensions.html:116:4 MANDATORY_ATTR_MISSING src or srcdoc (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-iframe/amp-iframe.md)
+feature_tests/mandatory_dimensions.html:117:4 MANDATORY_ATTR_MISSING src (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-pixel.md)
+feature_tests/mandatory_dimensions.html:120:4 DISALLOWED_ATTR src (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-fit-text/amp-fit-text.md)
+feature_tests/mandatory_dimensions.html:121:4 DISALLOWED_ATTR src (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-carousel/amp-carousel.md)
+feature_tests/mandatory_dimensions.html:122:4 DISALLOWED_ATTR srcset (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube/amp-youtube.md)
+feature_tests/mandatory_dimensions.html:123:4 DISALLOWED_ATTR srcset (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-twitter/amp-twitter.md)
+feature_tests/mandatory_dimensions.html:124:4 DISALLOWED_ATTR srcset (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-instagram/amp-instagram.md)
+feature_tests/mandatory_dimensions.html:125:4 DISALLOWED_ATTR src (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-lightbox/amp-lightbox.md)
+feature_tests/mandatory_dimensions.html:128:4 DISALLOWED_ATTR foo (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
diff --git a/validator/testdata/feature_tests/mandatory_style_without_spaces.html b/validator/testdata/feature_tests/mandatory_style_without_spaces.html
new file mode 100644
index 000000000000..0ea112782e7d
--- /dev/null
+++ b/validator/testdata/feature_tests/mandatory_style_without_spaces.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+Hello, world.
+
+
diff --git a/validator/testdata/feature_tests/mandatory_style_without_spaces.out b/validator/testdata/feature_tests/mandatory_style_without_spaces.out
new file mode 100644
index 000000000000..7ef22e9a431a
--- /dev/null
+++ b/validator/testdata/feature_tests/mandatory_style_without_spaces.out
@@ -0,0 +1 @@
+PASS
diff --git a/validator/testdata/minimum_valid_amp.html b/validator/testdata/feature_tests/minimum_valid_amp.html
similarity index 81%
rename from validator/testdata/minimum_valid_amp.html
rename to validator/testdata/feature_tests/minimum_valid_amp.html
index 806d7919cb17..71f66774aba3 100644
--- a/validator/testdata/minimum_valid_amp.html
+++ b/validator/testdata/feature_tests/minimum_valid_amp.html
@@ -1,5 +1,3 @@
-
-
+
+
+
-
+
Hello, world.
diff --git a/validator/testdata/feature_tests/minimum_valid_amp.out b/validator/testdata/feature_tests/minimum_valid_amp.out
new file mode 100644
index 000000000000..7ef22e9a431a
--- /dev/null
+++ b/validator/testdata/feature_tests/minimum_valid_amp.out
@@ -0,0 +1 @@
+PASS
diff --git a/validator/testdata/feature_tests/several_errors.html b/validator/testdata/feature_tests/several_errors.html
new file mode 100644
index 000000000000..07b9fd2c2dcf
--- /dev/null
+++ b/validator/testdata/feature_tests/several_errors.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/validator/testdata/feature_tests/several_errors.out b/validator/testdata/feature_tests/several_errors.out
new file mode 100644
index 000000000000..03cc0e379320
--- /dev/null
+++ b/validator/testdata/feature_tests/several_errors.out
@@ -0,0 +1,9 @@
+FAIL
+feature_tests/several_errors.html:23:2 INVALID_ATTR_VALUE charset=pick-a-key (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#chrs)
+feature_tests/several_errors.html:26:2 INVALID_ATTR_VALUE type=javascript
+feature_tests/several_errors.html:32:2 MANDATORY_ATTR_MISSING width (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
+feature_tests/several_errors.html:32:2 MANDATORY_ATTR_MISSING height (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-img.md)
+feature_tests/several_errors.html:33:0 DISALLOWED_ATTR made_up_attribute (see https://github.com/ampproject/amphtml/blob/master/builtins/amp-ad.md)
+feature_tests/several_errors.html:37:7 MANDATORY_TAG_MISSING charset utf-8 declaration (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#chrs)
+feature_tests/several_errors.html:37:7 MANDATORY_TAG_MISSING viewport declaration (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#vprt)
+feature_tests/several_errors.html:37:7 MANDATORY_TAG_MISSING amphtml engine v1.js script (see https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#scrpt)
diff --git a/validator/testdata/feature_tests/spec_example.html b/validator/testdata/feature_tests/spec_example.html
new file mode 100644
index 000000000000..ba73a481be59
--- /dev/null
+++ b/validator/testdata/feature_tests/spec_example.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ Sample document
+
+
+
+
+
+
+
+
+
+ Sample document
+
+ Some text
+
+
+
+
+
+
diff --git a/validator/testdata/feature_tests/spec_example.out b/validator/testdata/feature_tests/spec_example.out
new file mode 100644
index 000000000000..7ef22e9a431a
--- /dev/null
+++ b/validator/testdata/feature_tests/spec_example.out
@@ -0,0 +1 @@
+PASS
diff --git a/validator/testdata/feature_tests/svg.html b/validator/testdata/feature_tests/svg.html
new file mode 100644
index 000000000000..1b4960b14aff
--- /dev/null
+++ b/validator/testdata/feature_tests/svg.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ SVG
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Layer 1
+ Oh Hi I'm an SVG.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/validator/testdata/feature_tests/svg.out b/validator/testdata/feature_tests/svg.out
new file mode 100644
index 000000000000..7ef22e9a431a
--- /dev/null
+++ b/validator/testdata/feature_tests/svg.out
@@ -0,0 +1 @@
+PASS
diff --git a/validator/testdata/feature_tests/youtube.html b/validator/testdata/feature_tests/youtube.html
new file mode 100644
index 000000000000..d54867f6a662
--- /dev/null
+++ b/validator/testdata/feature_tests/youtube.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/validator/testdata/feature_tests/youtube.out b/validator/testdata/feature_tests/youtube.out
new file mode 100644
index 000000000000..3286d97269b7
--- /dev/null
+++ b/validator/testdata/feature_tests/youtube.out
@@ -0,0 +1,3 @@
+FAIL
+feature_tests/youtube.html:38:2 MANDATORY_ATTR_MISSING src or data-videoid or video-id (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube/amp-youtube.md)
+feature_tests/youtube.html:40:2 MUTUALLY_EXCLUSIVE_ATTRS src or data-videoid or video-id (see https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube/amp-youtube.md)
diff --git a/validator/validator_test.js b/validator/validator_test.js
new file mode 100644
index 000000000000..a413bee762ab
--- /dev/null
+++ b/validator/validator_test.js
@@ -0,0 +1,188 @@
+/**
+ * @license
+ * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
+ *
+ * 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.
+ */
+goog.require('amp.validator.validateString');
+goog.require('amp.validator.renderValidationResult');
+goog.provide('amp.validator.validatorTest');
+
+/**
+ * This function allows us to inject NodeJS dependencies into the test.
+ * It encloses the contents of the remainder of the file. This
+ * function gets called from a small NodeJS script (e.g. in our github
+ * project, see build.py, GenerateValidatorTest).
+ * @param {!Array} testdataDirs
+ * @param {?} assert The NodeJS assert module.
+ * @param {?} fs The NodeJS fs module.
+ * @param {?} path The NodeJS path module.
+ * @param {?} describe The describe function from jasmin.
+ * @param {?} it The it function from jasmin.
+ * @export
+ */
+amp.validator.validatorTest = function(testdataDirs, assert, fs,
+ path, describe, it) {
+
+/**
+ * Returns the absolute path for a given test file, that is, a file
+ * underneath a testdata directory. E.g., 'foo/bar/testdata/baz.html' =>
+ * 'baz.html'.
+ * @param {!string} testFile
+ * @return {!string}
+ */
+function absolutePathFor(testFile) {
+ for (const dir of testdataDirs) {
+ const candidate = path.join(dir, testFile);
+ if (fs.existsSync(candidate)) {
+ return candidate;
+ }
+ }
+ throw 'Could not find ' + testFile;
+}
+
+/**
+ * Returns all html files underneath the testdata directories. This does
+ * not traverse the directories recursively but only one level deep
+ * (e.g., it will find the 'feature_tests' subdir and the .html files inside it.
+ * @return {!Array}
+ */
+function findHtmlFilesRelativeToTestdata() {
+ const testFiles = [];
+ for (const dir of testdataDirs) {
+ for (const subdir of /** @type {!Array} */(
+ fs.readdirSync(path.join(dir)))) {
+ for (const candidate of /** @type {!Array} */(
+ fs.readdirSync(path.join(dir, subdir)))) {
+ if (candidate.match(/^.*.html/g)) {
+ testFiles.push(path.join(subdir, candidate));
+ }
+ }
+ }
+ }
+ return testFiles;
+}
+
+/**
+ * An AMP Validator test case. This constructor will load the AMP HTML file
+ * and also find the adjacent .out file.
+ * @constructor
+ */
+const ValidatorTestCase = function(ampHtmlFile) {
+ /** @type {!string} */
+ this.name = ampHtmlFile;
+ /** @type {!string} */
+ this.ampHtmlFile = ampHtmlFile;
+ /**
+ * This field can be null, indicating that the expectedOutput did not
+ * come from a file.
+ * @type {?string}
+ */
+ this.expectedOutputFile = path.join(
+ path.dirname(ampHtmlFile), path.basename(ampHtmlFile, '.html') + '.out');
+ /** @type {!string} */
+ this.ampHtmlFileContents = fs.readFileSync(
+ absolutePathFor(this.ampHtmlFile), 'utf8');
+ /** @type {!string} */
+ this.expectedOutput = fs.readFileSync(
+ absolutePathFor(this.expectedOutputFile), 'utf8').trim();
+};
+
+/**
+ * Runs the test, by executing the AMP Validator, then comparing its output
+ * against the golden file content.
+ */
+ValidatorTestCase.prototype.run = function() {
+ const results = amp.validator.validateString(this.ampHtmlFileContents);
+ const observed = amp.validator.renderValidationResult(
+ results, this.ampHtmlFile).join('\n');
+ if (observed === this.expectedOutput) {
+ return;
+ }
+ let message = '';
+ if (this.expectedOutputFile != null) {
+ message = '\n' + this.expectedOutputFile + ':1:0\n';
+ }
+ message += 'expected:\n' + this.expectedOutput + '\nsaw:\n' + observed;
+ assert.fail('', '', message, '');
+};
+
+/**
+ * A strict comparison between two values.
+ * Note: Unfortunately assert.strictEqual has some drawbacks, including that
+ * it truncates the provided arguments (and it's not configurable) and
+ * with the Closure compiler, it requires a message argument to which
+ * we'd always have to pass undefined. Too messy, so we roll our own.
+ */
+function assertStrictEqual(expected, saw) {
+ assert.ok(expected === saw, 'expected: ' + expected + ' saw: ' + saw);
+}
+
+describe('ValidatorFeatures', () => {
+ for (const htmlFile of findHtmlFilesRelativeToTestdata()) {
+ const test = new ValidatorTestCase(htmlFile);
+ it(test.name, () => { test.run(); });
+ }
+});
+
+describe('ValidatorCssLengthValidation', () => {
+ // Rather than encoding some really long author stylesheets in
+ // testcases, which would be difficult to read/verify that the
+ // testcase is valid, we modify a valid testcase
+ // (features/css_length.html) designed for this purpose in code.
+
+ // We use a blob of length 10 (both bytes and chars) to make it easy to
+ // construct stylesheets of any length that we want.
+ const validStyleBlob = 'h1 {a: b}\n';
+ assertStrictEqual(10, validStyleBlob.length);
+
+ it('accepts max bytes with exactly 50000 bytes in author stylesheet', () => {
+ const maxBytes = Array(5001).join(validStyleBlob);
+ assertStrictEqual(50000, maxBytes.length);
+
+ const test = new ValidatorTestCase('feature_tests/css_length.html');
+ test.ampHtmlFileContents = test.ampHtmlFileContents.replace(
+ '.replaceme {}', maxBytes);
+ });
+
+ it('will not accept 50001 bytes in author stylesheet - one too many', () => {
+ const oneTooMany = Array(5001).join(validStyleBlob) + ' ';
+ assertStrictEqual(50001, oneTooMany.length);
+ const test = new ValidatorTestCase('feature_tests/css_length.html');
+ test.ampHtmlFileContents = test.ampHtmlFileContents.replace(
+ '.replaceme {}', oneTooMany);
+ test.expectedOutputFile = null;
+ test.expectedOutput =
+ 'FAIL\n' +
+ 'feature_tests/css_length.html:28:2 STYLESHEET_TOO_LONG ' +
+ 'seen: 50001 bytes, limit: 50000 bytes ' +
+ '(see https://github.com/ampproject/amphtml/blob/master/spec/' +
+ 'amp-html-format.md#maximum-size)';
+ });
+
+ it('knows utf8 and rejects file w/ 50002 bytes but 49999 characters', () => {
+ const multiByteSheet = Array(5000).join(validStyleBlob) + 'h {a: 😺}';
+ assertStrictEqual(49999, multiByteSheet.length); // character length
+ const test = new ValidatorTestCase('feature_tests/css_length.html');
+ test.ampHtmlFileContents = test.ampHtmlFileContents.replace(
+ '.replaceme {}', multiByteSheet);
+ test.expectedOutputFile = null;
+ test.expectedOutput =
+ 'FAIL\n' +
+ 'feature_tests/css_length.html:28:2 STYLESHEET_TOO_LONG ' +
+ 'seen: 50002 bytes, limit: 50000 bytes ' +
+ '(see https://github.com/ampproject/amphtml/blob/master/spec/' +
+ 'amp-html-format.md#maximum-size)';
+ });
+});
+};