Commits

Kirill Simonov committed 12888f5

Ported regression tests to pbbt.

Comments (0)

Files changed (29)

 #pyflakes: pyflakes
 #coverage: coverage
 #htsql-ctl: htsql-ctl
+#pbbt: pbbt
 
 
 #

cogs.local/check.py

             if client_vm.missing():
                 continue
             client_vm.start()
+            client_vm.run("~/bin/pip -q install"
+                          " hg+http://bitbucket.org/prometheus/pbbt")
             sh("hg clone --ssh='ssh -F %s' . ssh://linux-vm/src/htsql"
                % (CTL_DIR+"/ssh_config"))
             errors += trial("hg update && python setup.py install",
                             "installing HTSQL under %s" % client_vm.name)
-            errors += trial("htsql-ctl regress -qi test/regress.yaml sqlite",
+            errors += trial("pbbt test/regress.yaml -q -S /all/sqlite",
                             "testing sqlite backend")
             for server_vm, suite in [(pgsql84_vm, 'pgsql'),
                                      (pgsql90_vm, 'pgsql'),
                 password_value = "admin"
                 host_value = "10.0.2.2"
                 port_value = 10000+server_vm.port
-                command = "%s=%s %s=%s %s=%s %s=%s" \
-                          " htsql-ctl regress -qi test/regress.yaml %s" \
-                          % (username_key, username_value,
+                command = "pbbt test/regress.yaml -q -S /all/%s" \
+                          " -D %s=%s -D %s=%s -D %s=%s -D %s=%s" \
+                          % (suite, username_key, username_value,
                              password_key, password_value,
-                             host_key, host_value, port_key, port_value,
-                             suite)
+                             host_key, host_value, port_key, port_value)
                 message = "testing %s backend against %s" \
                           % (suite, server_vm.name)
                 errors += trial(command, message)
                 server_vm.stop()
-            errors += trial("htsql-ctl regress -qi test/regress.yaml routine",
+            errors += trial("pbbt test/regress.yaml -q -S /all/routine",
                             "testing htsql-ctl routines")
             client_vm.stop()
     except:

cogs.local/data/vm/py26-update.sh

 apt-get -qy install python-yaml
 apt-get -qy install python-pip
 apt-get -qy install python-virtualenv
+apt-get -qy install python-argparse
 
 # Install development files for Python and database drivers.
 apt-get -qy install python2.6-dev

cogs.local/run.py

         exe(env.ctl_path+" "+command, environ=environ)
 
 
-def regress(command, environ=None):
-    # Run `htsql-ctl regress -i test/regress.yaml <command>`.
-    htsql_ctl("regress -i test/regress.yaml "+command, environ=environ)
+def regress(command):
+    # Run `pbbt test/regress.yaml <command>`.
+    variables = make_variables()
+    with env(debug=True):
+        sh(env.pbbt_path+" test/regress.yaml -E test/regress.py "
+           +variables+command)
 
 
-def exe_regress(command, environ=None):
-    # Run `htsql-ctl regress -i test/regress.yaml <command>`.
-    exe_htsql_ctl("regress -i test/regress.yaml "+command, environ=environ)
+def exe_regress(command):
+    # Run `pbbt test/regress.yaml <command>`.
+    variables = make_variables()
+    with env(debug=True):
+        exe(env.pbbt_path+" test/regress.yaml -E test/regress.py "
+            +variables+command)
 
 
 def pyflakes(command):
     if engine == 'sqlite':
         return "sqlite:build/regress/sqlite/htsql_%s.sqlite" % name
     elif engine in ['pgsql', 'mysql', 'mssql']:
-        host = getattr(env, '%s_host' % engine)
+        host = getattr(env, '%s_host' % engine) or ''
         if host:
             port = getattr(env, '%s_port' % engine)
             if port:
         return ("%s://htsql_%s:secret@%s/htsql_%s"
                 % (engine, name, host, name))
     elif engine == 'oracle':
-        host = env.oracle_host
+        host = env.oracle_host or ''
         if host and env.oracle_port:
             host = "%s:%s" % (host, env.oracle_port)
         sid = env.oracle_sid
         return "oracle://htsql_%s:secret@%s/%s" % (name, host, sid)
 
 
-def make_environ():
-    # Populate environment variables with database configuration.
-    environ = {}
+def make_variables():
+    # Generate conditional variables for pbbt.
+    variables = []
     for engine in ['pgsql', 'mysql', 'oracle', 'mssql']:
         for name in ['username', 'password', 'host', 'port']:
             value = getattr(env, '%s_%s' % (engine, name))
             if value:
-                environ['%s_%s' % (engine.upper(), name.upper())] = str(value)
-    if env.oracle_sid:
-        environ['ORACLE_SID'] = env.oracle_sid
-    return environ
+                variables.append('-D %s_%s=%s '
+                                 % (engine.upper(), name.upper(), str(value)))
+        if engine == 'oracle' and env.oracle_sid:
+            variables.append('-D ORACLE_SID=%s ' % env.oracle_sid)
+    return "".join(variables)
 
 
 def make_client(engine, name=None):
 
 
 @setting
+def PBBT(path=None):
+    """path to pbbt executable"""
+    if not path:
+        path = 'pbbt'
+    if not isinstance(path, str):
+        raise ValueError("expected a string value")
+    env.add(pbbt_path=path)
+
+
+@setting
 def HTSQL_HOST(host=None):
     """host of the demo HTSQL server"""
     env.add(htsql_host=host or 'localhost')
 @setting
 def PGSQL_USERNAME(name=None):
     """user name for PostgreSQL regression database"""
-    env.add(pgsql_username=name or '')
-    if not isinstance(env.pgsql_username, str):
+    if not (name is None or isinstance(name, str)):
         raise ValueError("expected a string value")
+    env.add(pgsql_username=name)
 
 
 @setting
 def PGSQL_PASSWORD(passwd=None):
     """password for PostgreSQL regression database"""
-    env.add(pgsql_password=passwd or '')
-    if not isinstance(env.pgsql_password, str):
+    if not (passwd is None or isinstance(passwd, str)):
         raise ValueError("expected a string value")
+    env.add(pgsql_password=passwd)
 
 
 @setting
 def PGSQL_HOST(host=None):
     """host for PostgreSQL regression database"""
-    env.add(pgsql_host=host or '')
-    if not isinstance(env.pgsql_host, str):
+    if not (host is None or isinstance(host, str)):
         raise ValueError("expected a string value")
+    env.add(pgsql_host=host)
 
 
 @setting
 def PGSQL_PORT(port=None):
     """port for PostgreSQL regression database"""
-    env.add(pgsql_port=port or 0)
-    if not isinstance(env.pgsql_port, int):
+    if not (port is None or isinstance(port, int)):
         raise ValueError("expected an integer value")
+    env.add(pgsql_port=port)
 
 
 @setting
 def MYSQL_USERNAME(name=None):
     """user name for MySQL regression database"""
-    env.add(mysql_username=name or '')
-    if not isinstance(env.mysql_username, str):
+    if not (name is None or isinstance(name, str)):
         raise ValueError("expected a string value")
+    env.add(mysql_username=name)
 
 
 @setting
 def MYSQL_PASSWORD(passwd=None):
     """password for MySQL regression database"""
-    env.add(mysql_password=passwd or '')
-    if not isinstance(env.mysql_password, str):
+    if not (passwd is None or isinstance(passwd, str)):
         raise ValueError("expected a string value")
+    env.add(mysql_password=passwd)
 
 
 @setting
 def MYSQL_HOST(host=None):
     """host for MySQL regression database"""
-    env.add(mysql_host=host or '')
-    if not isinstance(env.mysql_host, str):
+    if not (host is None or isinstance(host, str)):
         raise ValueError("expected a string value")
+    env.add(mysql_host=host)
 
 
 @setting
 def MYSQL_PORT(port=None):
     """port for MySQL regression database"""
-    env.add(mysql_port=port or 0)
-    if not isinstance(env.mysql_port, int):
+    if not (port is None or isinstance(port, int)):
         raise ValueError("expected an integer value")
+    env.add(mysql_port=port)
 
 
 @setting
 def ORACLE_USERNAME(name=None):
     """user name for Oracle regression database"""
-    env.add(oracle_username=name or '')
-    if not isinstance(env.oracle_username, str):
+    if not (name is None or isinstance(name, str)):
         raise ValueError("expected a string value")
+    env.add(oracle_username=name)
 
 
 @setting
 def ORACLE_PASSWORD(passwd=None):
     """password for Oracle regression database"""
-    env.add(oracle_password=passwd or '')
-    if not isinstance(env.oracle_password, str):
+    if not (passwd is None or isinstance(passwd, str)):
         raise ValueError("expected a string value")
+    env.add(oracle_password=passwd)
 
 
 @setting
 def ORACLE_HOST(host=None):
     """host for Oracle regression database"""
-    env.add(oracle_host=host or '')
-    if not isinstance(env.oracle_host, str):
+    if not (host is None or isinstance(host, str)):
         raise ValueError("expected a string value")
+    env.add(oracle_host=host)
+
+
+@setting
+def ORACLE_PORT(port=None):
+    """port for Oracle regression database"""
+    if not (port is None or isinstance(port, int)):
+        raise ValueError("expected an integer value")
+    env.add(oracle_port=port)
 
 
 @setting
 def ORACLE_SID(sid=None):
     """SID for Oracle regression database"""
+    if not (sid is None or isinstance(sid, str)):
+        raise ValueError("expected a string value")
     env.add(oracle_sid=sid or 'XE')
-    if not isinstance(env.oracle_sid, str):
-        raise ValueError("expected a string value")
-
-
-@setting
-def ORACLE_PORT(port=None):
-    """port for Oracle regression database"""
-    env.add(oracle_port=port or 0)
-    if not isinstance(env.oracle_port, int):
-        raise ValueError("expected an integer value")
 
 
 @setting
 def MSSQL_USERNAME(name=None):
     """user name for MS SQL Server regression database"""
-    env.add(mssql_username=name or '')
-    if not isinstance(env.mssql_username, str):
+    if not (name is None or isinstance(name, str)):
         raise ValueError("expected a string value")
+    env.add(mssql_username=name)
 
 
 @setting
 def MSSQL_PASSWORD(passwd=None):
     """password for MS SQL Server regression database"""
-    env.add(mssql_password=passwd or '')
-    if not isinstance(env.mssql_password, str):
+    if not (passwd is None or isinstance(passwd, str)):
         raise ValueError("expected a string value")
+    env.add(mssql_password=passwd)
 
 
 @setting
 def MSSQL_HOST(host=None):
     """host for MS SQL Server regression database"""
-    env.add(mssql_host=host or '')
-    if not isinstance(env.mssql_host, str):
+    if not (host is None or isinstance(host, str)):
         raise ValueError("expected a string value")
+    env.add(mssql_host=host)
 
 
 @setting
 def MSSQL_PORT(port=None):
     """port for MS SQL Server regression database"""
-    env.add(mssql_port=port or 0)
-    if not isinstance(env.mssql_port, int):
+    if not (port is None or isinstance(port, int)):
         raise ValueError("expected an integer value")
+    env.add(mssql_port=port)
 
 
 @task
       mssql                    : test MS SQL Server backend
     """
     command = "-q"
-    if suites:
-        command += " "+" ".join(suites)
-    exe_regress(command, environ=make_environ())
+    for suite in suites:
+        if not suite.startswith('/'):
+            suite = '/all/'+suite
+        command += " -S "+suite
+    exe_regress(command)
 
 
 @task
     command = "--train"
     if suites:
         command += " "+" ".join(suites)
-    exe_regress(command, environ=make_environ())
+    for suite in suites:
+        if not suite.startswith('/'):
+            suite = '/all/'+suite
+        command += " -S "+suite
+    exe_regress(command)
 
 
 @task
     Run this task to remove stale output data for deleted
     or modified tests.
     """
-    exe_regress("-q --train --purge", environ=make_environ())
+    exe_regress("-q --train --purge")
 
 
 @task
                 " --source=htsql,htsql_sqlite,htsql_pgsql,htsql_oracle,"
                 "htsql_mssql,htsql_django"
                 " `which \"%s\"` regress -i test/regress.yaml -q"
-                % env.ctl_path,
+                % env.pbbt_path,
                 environ=environ)
     coverage_py("html --directory=build/coverage",
                 "./build/coverage/coverage.dat")
       sandbox                  : empty database
     """
     db = make_db(engine, 'demo')
-    regress("-q drop-%s create-%s" % (engine, engine), environ=make_environ())
+    regress("-q -S /all/%s/dropdb -S /all/%s/createdb" % (engine, engine))
     log()
     log("The demo regression database has been deployed at:")
     log("  `{}`", db)
       `cogs help createdb`
     """
     db = make_db(engine, 'demo')
-    regress("-q drop-%s" % engine, environ=make_environ())
+    regress("-q -S /all/%s/dropdb" % engine)
     log()
     log("Regression databases has beeen deleted.")
     log()

test/code/test_embedding.py

 from htsql import HTSQL
 import sys, decimal, datetime
 
-db = str(state.app.htsql.db)
+db = __pbbt__['demo'].db
 
 htsql = HTSQL(db)
 
 integer_value = 3571
 float_value = -57721e-5
 decimal_value = decimal.Decimal("0.875")
-if 'sqlite' in state.toggles:
+if 'sqlite' in __pbbt__:
     decimal_value = None
 date_value = datetime.date(2010, 4, 15)
 time_value = datetime.time(20, 3)

test/input/addon.yaml

 #
 
 title: HTSQL Extensions
-id: addon
+suite: addon
 tests:
 
 # TWEAK - tweaks for HTSQL
     extensions:
       tweak.csrf: {}
   - uri: /
-    ignore-headers: true
+    ignore: &ignore-htsql-csrf-token |
+      Set-Cookie:.htsql-csrf-token=([0-9A-Za-z]+)
   - uri: /school
-    ignore-headers: true
+    ignore: *ignore-htsql-csrf-token
     expect: 403
 
   # Passing CSRF token back to the server
-  - py: pass-csrf-token
-    code: |
+  - py: |
+      # pass-csrf-token
       import wsgiref.util
       class response:
           status = None
           wsgiref.util.setup_testing_defaults(environ)
           response.status = None
           response.headers = None
-          response.body = ''.join(state.app(environ, start_response))
+          response.body = ''.join(__pbbt__['htsql'](environ, start_response))
           return (response.status, response.headers, response.body)
       status, headers, body = request({'PATH_INFO': "/"})
       token = None
       tweak.csrf:
         allow_cs_read: true
   - uri: /school
-    ignore-headers: true
+    ignore: *ignore-htsql-csrf-token
 
 # TWEAK.DJANGO - adapt to Django
-- py: has-django
-  ifdef: [sqlite, pgsql, mysql, oracle]
-  code: |
+- py: |
+    # has-django
     try:
         import django
         if django.VERSION > (1, 3):
-            state.toggles.add('django')
+            __pbbt__['django'] = True
     except ImportError:
         pass
+  if: [sqlite, pgsql, mysql, oracle]
 - title: tweak.django
-  ifdef: django
+  if: django
   tests:
   # Addon description
   - ctl: [ext, tweak.django]
 
   # Load `test_django_sandbox` and deploy the database
-  - py: add-module-path
-    code: |
+  - py: |
+      # add-module-path
       import __builtin__, sys, os, os.path
+      from htsql.core.util import DB
       path = os.path.join(os.getcwd(), "test/code")
       sys.path.insert(0, path)
-      __builtin__.sandbox = state.saves['sandbox'][0]
+      __builtin__.sandbox = DB.parse(__pbbt__['sandbox'].db)
       os.environ['DJANGO_SETTINGS_MODULE'] = 'test_django_sandbox.settings'
       from test_django_sandbox import createdb
       createdb()
     skip: true
 
   # Restore the original `sys.path`
-  - py: remove-module-path
-    code: |
+  - py: |
+      # remove-module-path
       from test_django_sandbox import dropdb
       dropdb()
       import __builtin__, sys, os, os.path
 
 # TWEAK.FILEDB - make a database from a set of CSV files
 - title: tweak.filedb
-  ifdef: sqlite
+  if: sqlite
   tests:
   # Addon description
   - ctl: [ext, tweak.filedb]
   - uri: /{_source[permanent].timestamp<_source[volatile].timestamp}
 
   # Cleanup
-  - remove:
+  - rm:
     - build/regress/table.csv
     - build/regress/Names.csv
     - build/regress/irregular-fields.csv
 
 # TWEAK.GATEWAY - define gateways to other databases
 - title: tweak.gateway
-  ifdef: mysql
+  if: mysql
   tests:
   # Addon description
   - ctl: [ext, tweak.gateway]
 
 # TWEAK.INET - IPv4 data type
 - title: tweak.inet
-  ifdef: pgsql
+  if: pgsql
   tests:
   # Addon description
   - ctl: [ext, tweak.inet]
       tweak.csrf: {}
       tweak.meta: {}
   - uri: /meta(/table)
-    ignore-headers: true
+    ignore: *ignore-htsql-csrf-token
     expect: 403
 
 # TWEAK.OVERRIDE - adjust database metadata
     extensions:
       tweak.override:
         included-tables: [ad.*]
-    ifdef: [pgsql, mssql]
+    if: [pgsql, mssql]
   - uri: /count(program)
-    ifdef: [pgsql, mssql]
+    if: [pgsql, mssql]
   - uri: /count(student)
     expect: 400
-    ifdef: [pgsql, mssql]
+    if: [pgsql, mssql]
 
   # Test `included-columns`
   - load: demo
     ignore: true
 
 # TWEAK.SQLALCHEMY - adapt to SQLAlchemy
-- py: has-sqlalchemy
-  code: |
+- py: |
+    # has-sqlalchemy
     try:
         import sqlalchemy
         if sqlalchemy.__version__ > '0.7.':
-            state.toggles.add('sqlalchemy')
+            __pbbt__['sqlalchemy'] = True
     except ImportError:
         pass
 - title: tweak.sqlalchemy
-  ifdef: sqlalchemy
+  if: sqlalchemy
   tests:
   # Addon description
   - ctl: [ext, tweak.sqlalchemy]
 
   # Make sure `test_sqlalchemy_*` could be found
-  - py: add-module-path
-    code: |
+  - py: |
+      # add-module-path
       import __builtin__, sys, os, os.path
+      from htsql.core.util import DB
       path = os.path.join(os.getcwd(), "test/code")
       sys.path.insert(0, path)
-      __builtin__.demo = state.saves['demo'][0]
-      __builtin__.sandbox = state.saves['sandbox'][0]
+      __builtin__.demo = DB.parse(__pbbt__['demo'].db)
+      __builtin__.sandbox = DB.parse(__pbbt__['sandbox'].db)
       from test_sqlalchemy_sandbox import createdb
       createdb()
 
   - uri: /addresses{user.name, email_address}
 
   # Restore the original `sys.path`
-  - py: remove-module-path
-    code: |
+  - py: |
+      # remove-module-path
       from test_sqlalchemy_sandbox import dropdb
       dropdb()
       import __builtin__, sys, os, os.path
 
 # TWEAK.SYSTEM - add access to system tables
 - title: tweak.system
-  ifdef: pgsql
+  if: pgsql
   tests:
   # Addon description
   - ctl: [ext, tweak.system]
 
 # TWEAK.TIMEOUT - limit query execution time
 - title: tweak.system
-  ifdef: pgsql
+  if: pgsql
   tests:
   # Addon description
   - ctl: [ext, tweak.timeout]

test/input/embedding.yaml

 #
 
 title: Embedding HTSQL
-id: embedding
+suite: embedding
 tests:
-- py-include: test/code/test_embedding.py
+- py: test/code/test_embedding.py
 

test/input/error.yaml

 #
 
 title: Error Reporting
-id: error
+suite: error
 tests:
 
 - title: Scan Errors
   tests:
   # invalid integer value
   - uri: /18446744073709551616
-    ifndef: [oracle]
+    unless: [oracle]
     expect: 400
 
 

test/input/etl.yaml

 #
 
 title: ETL/CRUD
-id: etl
-ifdef: pgsql
+suite: etl
+if: pgsql
 tests:
 - load: etl
 - ctl: [ext, tweak.etl]

test/input/format.yaml

 #
 
 title: Formatting Output Data
-id: format
+suite: format
 tests:
 
 - title: Supported Output Formats

test/input/library.yaml

 #
 
 title: Standard Data Types, Functions, and Operations
-id: library
+suite: library
 tests:
 
 ########################################################################
   - uri: /{text(''), text('HTSQL'), text('O''Reilly'),
            text('%ce%bb%cf%8c%ce%b3%ce%bf%cf%82'),
            text('$-b \pm \sqrt{b^2 - 4ac} \over 2a$')}
-    ifndef: mssql
+    unless: mssql
   # The regression database for MS SQL Server uses Latin1 locale and therefore
   # is unable to represent greek characters.
   - uri: /{text(''), text('HTSQL'), text('O''Reilly'),
            text('zo%c3%b6logy'),
            text('$-b \pm \sqrt{b^2 - 4ac} \over 2a$')}
-    ifdef: mssql
+    if: mssql
   - uri: /{text('832040')}
 
   # Integer values
   # Out of range
   - uri: /{18446744073709551616}
     expect: 400
-    ifndef: oracle
+    unless: oracle
   # Oracle does not have a range limitation for the `INTEGER` data type.
   - uri: /{18446744073709551616, 340282366920938463463374607431768211456}
-    ifdef: oracle
+    if: oracle
 
   # Decimal values
   - uri: /{1.0, -2.5, 0.875}
   - uri: /{decimal('1E-10')}
   # Arbitrary length
   - uri: /{4154781481226426191177580544000000.808017424794512875886459904961710757005754368000000000}
-    ifndef: mssql
+    unless: mssql
   # Invalid decimal literals
   - uri: /{decimal('vingt-cinq')}
     expect: 400
     expect: 400
   # Not a number
   - uri: /{integer(text('zero'))}
-    ifdef: [sqlite, mysql]
+    if: [sqlite, mysql]
   - uri: /{float(text('zero'))}
-    ifdef: [sqlite, mysql]
+    if: [sqlite, mysql]
   - uri: /{integer(text('cinq'))}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql]
+    unless: [sqlite, mysql]
   # Integer overflow
   - uri: /{integer(4294967296.0)}
     expect: 409
     ignore: true
-    ifdef: [pgsql, mssql]
+    if: [pgsql, mssql]
   - uri: /{integer(1.8446744073709552e+19)}
     expect: 409
     ignore: true
-    ifdef: [pgsql, mssql]
+    if: [pgsql, mssql]
   - uri: /{integer(4294967296.0),
            integer(1.8446744073709552e+19)}
-    ifndef: [pgsql, mssql]
+    unless: [pgsql, mssql]
 
   # Arithmetics
   - uri: /{+7, -7, +2.125, -2.125, +271828e-5, -271828e-5}
   - uri: /{7*2147483647}
     expect: 409
     ignore: true
-    ifdef: [pgsql, mssql]
+    if: [pgsql, mssql]
   - uri: /{9223372036854775807+1}
     # FIXME: silent overflow under MySQL 5.1; error under MySQL 5.5.
     expect: 409
     ignore: true
-    ifdef: [pgsql, mssql]
+    if: [pgsql, mssql]
   - uri: /{7*2147483647, 9223372036854775807+1}
     # FIXME: Older SQLite silently truncates the result, newer SQLite
     # converts the result to float.
-    ifdef: [oracle]
+    if: [oracle]
   # Division by zero
   - uri: /{7/0}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql]
+    unless: [sqlite, mysql]
   - uri: /{7/0.0}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql]
+    unless: [sqlite, mysql]
   - uri: /{7/0e0}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql, oracle]
+    unless: [sqlite, mysql, oracle]
   - uri: /{7/0, 7/0.0, 7/0e0}
-    ifdef: [sqlite, mysql]
+    if: [sqlite, mysql]
   - uri: /{0e0/0e0, 7/0e0}
-    ifdef: [oracle]
+    if: [oracle]
 
   # Rounding and Truncating
   - uri: /{round(3272.78125), round(3272.78125,2), round(3272.78125,-2)}
   # Bug in MSSQL 2008R2, see
   # https://connect.microsoft.com/SQLServer/feedback/details/646516/bad-example-for-round
   - uri: /{round(9973,-2)}
-    ifndef: [mssql]
+    unless: [mssql]
   # Inadmissible operand
   - uri: /{round(271828e-5,2)}
     expect: 400
   # Bug in MSSQL 2005, `SELECT (CASE WHEN ('LOL' LIKE NULL) THEN 1 END)`
   # generates [<1>].
   - uri: /{null()~'LOL', 'LOL'~null(), null()~null()}
-    ifndef: mssql
+    unless: mssql
   - uri: /{null()~'LOL', null()~null()}
-    ifdef: mssql
+    if: mssql
   - uri: /this(){true()}?'LOL'~null()
-    ifdef: mssql
+    if: mssql
 
   # Slicing
   - uri: /{head('OMGWTFBBQ'), head('OMGWTFBBQ',3), head('OMGWTFBBQ',-3)}
   - uri: /{replace('OMGWTFBBQ','WTF','LOL')}
   - uri: /{replace('OMGWTFBBQ','wtf','LOL'),
            replace('OMGWTFBBQ','WTF','lol')}
-    ifndef: [mssql, oracle]
+    unless: [mssql, oracle]
   # `REPLACE` in MSSQL respects the database collation, which is
   # case-insensitive for the regression database.  Same with Oracle
   # when `NLS_SORT = BINARY_CI` and `NLS_COMP = LINGUISTIC`.
   - uri: /{replace('OMGWTFBBQ','wtf','LOL')}
-    ifdef: [mssql, oracle]
+    if: [mssql, oracle]
   - uri: /{replace('OMGWTFBBQ','WTF','lol')}
-    ifdef: [mssql, oracle]
+    if: [mssql, oracle]
   - uri: /{replace('floccinaucinihilipilification','ili','LOL')}
   - uri: /{replace('OMGWTFBBQ','','LOL'),
            replace('OMGWTFBBQ','WTF','')}
   - uri: /{date(text('birthday'))}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql]
+    unless: [sqlite, mysql]
   - uri: /{date(text('2010-13-07'))}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql]
+    unless: [sqlite, mysql]
   - uri: /{date(text('birthday')),
            date(text('2010-13-07'))}
-    ifdef: [sqlite, mysql]
+    if: [sqlite, mysql]
 
   # Construction
   - uri: /{today()}
-    ignore: true
+    ignore: \d{4}-\d{2}-\d{2}
   - uri: /{date(2010,4,15), date(2010,3,46), date(2011,-8,15)}
 
   # Components
   # Conversion
   - uri: /{time(null()), time('20:03')}
   - uri: /{time(text('20:03'))}
-    ifndef: oracle
+    unless: oracle
   - uri: /{time(text('20:03:00'))}
-    ifdef: oracle
+    if: oracle
   - uri: /{time(datetime('2010-04-15 20:13'))}
   # Inadmissible operand
   - uri: /{time('29:04')}
   - uri: /{time(text('just a moment ago'))}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql]
+    unless: [sqlite, mysql]
   - uri: /{time(text('29:04'))}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql]
+    unless: [sqlite, mysql]
   - uri: /{time(text('just a moment ago')),
            time(text('29:04'))}
-    ifdef: [sqlite, mysql]
+    if: [sqlite, mysql]
 
   # Components
   - uri: /{hour(time('20:13:04.5')),
   # Conversion
   - uri: /{datetime(null()), datetime('2010-04-15 20:13')}
   - uri: /{datetime(text('2010-04-15 20:13'))}
-    ifndef: oracle
+    unless: oracle
   - uri: /{datetime(text('2010-04-15 20:13:00'))}
-    ifdef: oracle
+    if: oracle
   - uri: /{datetime(date('2010-04-15'))}
   # Inadmissible operand
   - uri: /{datetime('2010-13-07 17:43')}
   - uri: /{datetime(text('just a moment ago'))}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql]
+    unless: [sqlite, mysql]
   - uri: /{datetime(text('2010-13-07 17:43'))}
     expect: 409
     ignore: true
-    ifndef: [sqlite, mysql]
+    unless: [sqlite, mysql]
   - uri: /{datetime(text('just a moment ago')),
            datetime(text('2010-13-07 17:43'))}
-    ifdef: [sqlite, mysql]
+    if: [sqlite, mysql]
 
   # Construction
   - uri: /{now()}
   - uri: /{exists(course?department_code='lang'),
            every(course?department_code='lang'),
            count(course?department_code='lang')}
-    ifndef: oracle
+    unless: oracle
   - uri: /{exists(course?department_code='lang'),
            every(course?department_code='lang')}
-    ifdef: oracle
+    if: oracle
   - uri: /{count(course?department_code='lang')}
-    ifdef: oracle
+    if: oracle
   # Applied to an empty set
   - uri: /course?department_code='str'
   - uri: /{exists(course?department_code='str'),
            every(course?department_code='str'),
            count(course?department_code='str')}
-    ifndef: oracle
+    unless: oracle
   # Applied to all-TRUE, all-FALSE, mixed sets
   - uri: /course{department_code,no,credits,credits>3}
                 ?department_code={'me','mth','phys'}
   - uri: /{exists(course{credits>3}?department_code='me'),
            every(course{credits>3}?department_code='me'),
            count(course{credits>3}?department_code='me')}
-    ifndef: oracle
+    unless: oracle
   - uri: /{exists(course{credits>3}?department_code='mth'),
            every(course{credits>3}?department_code='mth'),
            count(course{credits>3}?department_code='mth')}
-    ifndef: oracle
+    unless: oracle
   - uri: /{exists(course{credits>3}?department_code='phys'),
            every(course{credits>3}?department_code='phys'),
            count(course{credits>3}?department_code='phys')}
-    ifndef: oracle
+    unless: oracle
   # Coercion
   - uri: /department{code,school.code,boolean(school)}
   - uri: /{exists(department{school}),
            every(department{school}),
            count(department{school})}
-    ifndef: oracle
+    unless: oracle
   # Singular operand
   - uri: /{exists(true())}
     expect: 400

test/input/mssql.yaml

 #
 
 title: MS SQL Server regression tests
-id: mssql
+suite: mssql
 output: test/output/mssql.yaml
 tests:
 
 - title: Remove any existing regression databases
-  id: drop-mssql
+  suite: dropdb
   tests:
   - connect: &connect-admin
       engine: mssql
     ignore: true
 
 - title: Deploy the regression databases
-  id: create-mssql
+  suite: createdb
   tests:
   # Create the `demo` database
   - connect: *connect-admin
       password: secret
       host: ${MSSQL_HOST}
       port: ${MSSQL_PORT}
-    sql-include: test/sql/demo-mssql.sql
+    sql: test/sql/demo-mssql.sql
   - db: *connect-demo
-  - py-include: test/sql/demo-load.py
+  - py: test/sql/demo-load.py
   # Create the `edge` database
   - connect: *connect-admin
     sql: |
       password: secret
       host: ${MSSQL_HOST}
       port: ${MSSQL_PORT}
-    sql-include: test/sql/edge-mssql.sql
+    sql: test/sql/edge-mssql.sql
   # Create the `sandbox` database
   - connect: *connect-admin
     sql: |
         -- The `sandbox` database is populated by the tests.
 
 - title: Run the test collection
-  id: test-mssql
+  suite: test
   tests:
-  - define: mssql
+  - set: mssql
   - db: *connect-sandbox
     extensions:
       htsql: {debug: true}

test/input/mysql.yaml

 #
 
 title: MySQL regression tests
-id: mysql
+suite: mysql
 output: test/output/mysql.yaml
 tests:
 
 - title: Remove any existing regression database
-  id: drop-mysql
+  suite: dropdb
   tests:
   - connect: &connect-admin
       engine: mysql
     ignore: true
 
 - title: Deploy the regression database
-  id: create-mysql
+  suite: createdb
   tests:
   # Create the `demo` database
   - connect: *connect-admin
       password: secret
       host: ${MYSQL_HOST}
       port: ${MYSQL_PORT}
-    sql-include: test/sql/demo-mysql.sql
+    sql: test/sql/demo-mysql.sql
   - db: *connect-demo
-  - py-include: test/sql/demo-load.py
+  - py: test/sql/demo-load.py
   # Create the `edge` database
   - connect: *connect-admin
     sql: |
       password: secret
       host: ${MYSQL_HOST}
       port: ${MYSQL_PORT}
-    sql-include: test/sql/edge-mysql.sql
+    sql: test/sql/edge-mysql.sql
   # Create the `sandbox` database
   - connect: *connect-admin
     sql: |
         -- The `sandbox` database is populated by the tests.
 
 - title: Run the test collection
-  id: test-mysql
+  suite: test
   tests:
-  - define: mysql
+  - set: mysql
   - db: *connect-sandbox
     extensions:
       htsql: {debug: true}

test/input/oracle.yaml

 #
 
 title: Oracle regression tests
-id: oracle
+suite: oracle
 output: test/output/oracle.yaml
 tests:
 
 - title: Remove any existing regression database
-  id: drop-oracle
+  suite: dropdb
   tests:
   - connect: &connect-admin
       engine: oracle
     ignore: true
 
 - title: Deploy the regression database
-  id: create-oracle
+  suite: createdb
   tests:
   # Create the `demo` database
   - connect: *connect-admin
       password: secret
       host: ${ORACLE_HOST}
       port: ${ORACLE_PORT}
-    sql-include: test/sql/demo-oracle.sql
+    sql: test/sql/demo-oracle.sql
   - db: *connect-demo
-  - py-include: test/sql/demo-load.py
+  - py: test/sql/demo-load.py
   # Create the `edge` database
   - connect: *connect-admin
     sql: |
       password: secret
       host: ${ORACLE_HOST}
       port: ${ORACLE_PORT}
-    sql-include: test/sql/edge-oracle.sql
+    sql: test/sql/edge-oracle.sql
   # Create the `sandbox` database
   - connect: *connect-admin
     sql: |
         -- The `sandbox` database is populated by the tests.
 
 - title: Run the test collection
-  id: test-oracle
+  suite: test
   tests:
-  - define: oracle
+  - set: oracle
   - db: *connect-sandbox
     extensions:
       htsql: {debug: true}

test/input/pgsql.yaml

 #
 
 title: PostgreSQL regression tests
-id: pgsql
+suite: pgsql
 output: test/output/pgsql.yaml
 tests:
 
 - title: Remove any existing regression database
-  id: drop-pgsql
+  suite: dropdb
   tests:
   - connect: &admin-connect
       engine: pgsql
     autocommit: true
 
 - title: Deploy the regression database
-  id: create-pgsql
+  suite: createdb
   tests:
   # Create the `demo` database
   - connect: *admin-connect
       password: secret
       host: ${PGSQL_HOST}
       port: ${PGSQL_PORT}
-    sql-include: test/sql/demo-pgsql.sql
+    sql: test/sql/demo-pgsql.sql
   - db: *connect-demo
-  - py-include: test/sql/demo-load.py
+  - py: test/sql/demo-load.py
   # Create the `edge` database
   - connect: *admin-connect
     sql: |
       password: secret
       host: ${PGSQL_HOST}
       port: ${PGSQL_PORT}
-    sql-include: test/sql/edge-pgsql.sql
+    sql: test/sql/edge-pgsql.sql
   # Create the `etl` database
   - connect: *admin-connect
     sql: |
       password: secret
       host: ${PGSQL_HOST}
       port: ${PGSQL_PORT}
-    sql-include: test/sql/etl-pgsql.sql
+    sql: test/sql/etl-pgsql.sql
   # Create the `sandbox` database
   - connect: *admin-connect
     sql: |
         -- The `sandbox` database is populated by the tests.
 
 - title: Run the test collection
-  id: test-pgsql
+  suite: test
   tests:
-  - define: pgsql
+  - set: pgsql
   - db: *connect-sandbox
     extensions:
       htsql: {debug: true}

test/input/routine.yaml

 #
 
 title: HTSQL-CTL Command-Line Tool
-id: routine
+suite: routine
 output: test/output/routine.yaml
 tests:
 
   - ctl: [shell, -C, "build/regress/sqlite/htsql_demo.yaml"]
     stdin: |
       /count(school)
-  - remove: [build/regress/sqlite/htsql_demo.yaml]
+  - rm: build/regress/sqlite/htsql_demo.yaml
   # Multiple extension options
   - ctl: [shell, *db, -E, "htsql:debug=true", -E, "tweak.meta"]
     stdin: |
       post build/regress/post.json /school
       post build/regress/post.data application/x-www-form-urlencoded /school
       post error /school
-  - remove: [build/regress/post.json, build/regress/post.data]
+  - rm: [build/regress/post.json, build/regress/post.data]
   # Run
   - write: build/regress/run.htsql
     data: |
       run
       run build/regress/run.htsql
       run error
-  - remove: [build/regress/run.htsql]
+  - rm: build/regress/run.htsql
 
 # Server routine
 - title: htsql-ctl server
   # Default address
   - start-ctl: &server-1 [server, *db, -q]
     sleep: 1
-  - py: GET-1
-    code: |
+  - py: |
+      # GET-1
       import time, urllib
       tries = 0
-      while tries < 5:
+      while tries < 60:
           try:
               print urllib.urlopen("http://127.0.0.1:8080/count(school)").read()
               break
           except:
               tries += 1
               time.sleep(0.5)
+      else:
+          print "Unable to connect to the server!"
   - end-ctl: *server-1
 
   # Custom address
   - start-ctl: &server-2 [server, *db, --host, "127.0.0.1", --port, "8088", -q]
     sleep: 1
-  - py: GET-2
-    code: |
+  - py: |
+      # GET-2
       import time, urllib
       tries = 0
-      while tries < 5:
+      while tries < 60:
           try:
               print urllib.urlopen("http://127.0.0.1:8088/count(school)").read()
               break
           except:
               tries += 1
               time.sleep(0.5)
+      else:
+          print "Unable to connect to the server!"
   - end-ctl: *server-2
 
 

test/input/schema.yaml

 #
 
 title: The Regression Schema
-id: schema
+suite: schema
 tests:
 
 - title: Tables

test/input/sqlite.yaml

 #
 
 title: SQLite regression tests
-id: sqlite
+suite: sqlite
 output: test/output/sqlite.yaml
 tests:
 
 - title: Remove any existing regression database
-  id: drop-sqlite
+  suite: dropdb
   tests:
   - rmdir: build/regress/sqlite
 
 - title: Deploy the regression database
-  id: create-sqlite
+  suite: createdb
   tests:
   - mkdir: build/regress/sqlite
   - write: build/regress/sqlite/htsql_demo.sqlite
   - connect: &connect-demo
       engine: sqlite
       database: build/regress/sqlite/htsql_demo.sqlite
-    sql-include: test/sql/demo-sqlite.sql
+    sql: test/sql/demo-sqlite.sql
   - db: *connect-demo
-  - py-include: test/sql/demo-load.py
+  - py: test/sql/demo-load.py
   - write: build/regress/sqlite/htsql_edge.sqlite
     data: ""
   - connect: &connect-edge
       engine: sqlite
       database: build/regress/sqlite/htsql_edge.sqlite
-    sql-include: test/sql/edge-sqlite.sql
+    sql: test/sql/edge-sqlite.sql
   - write: build/regress/sqlite/htsql_sandbox.sqlite
     data: ""
   - connect: &connect-sandbox
         -- The `sandbox` database is populated by the tests.
 
 - title: Run the test collection
-  id: test-sqlite
+  suite: test
   tests:
-  - define: sqlite
+  - set: sqlite
   - db: *connect-sandbox
     extensions:
       htsql: {debug: true}

test/input/translation.yaml

 #
 
 title: Edge Cases of HTSQL-to-SQL Translation
-id: translation
+suite: translation
 tests:
 
 # FIXME: update and refurbish!
     - uri: /count(school?exists(department))
     - uri: /{exists(school?!exists(department)),
              count(school?!exists(department))}
-      ifndef: oracle # Oracle cannot handle EXISTS and an aggregate in
+      unless: oracle # Oracle cannot handle EXISTS and an aggregate in
                      # the same SELECT clause.
     - uri: /{count(course),min(course.credits),
                            max(course.credits),
                            avg(course.credits)}
     - uri: /{count(school),count(department),count(course)}
     - uri: /{count(department),count(department?exists(course))}
-      ifndef: mssql # MSSQL does not allow an aggregate to contain
+      unless: mssql # MSSQL does not allow an aggregate to contain
                     # an EXISTS subquery.
     - uri: /school{code, campus, exists(department?count(course)>20),
                                  exists(program?count(student)>10)}
     # MSSQL does not allow the argument of an aggregate to contain
     # a subquery.
     - uri: /school{*,count(department.exists(course))}
-      ifndef: mssql
+      unless: mssql
     - uri: /school{code,count(department),count(program)}
     - uri: /school{code,exists(department),exists(program)}
     # Signular unit with a plural base flow.
     - uri: /course{department.code,no,credits}?credits=max(fork(department_code).credits)
     - uri: /department.course{department.code,no,credits}?credits=max(fork().credits)
     - uri: /course{department.code,no,credits,count(class)}?credits=max(fork(exists(class)).credits)
-      ifndef: [oracle, mssql] # subqueries are not permitted in the GROUP BY clause
+      unless: [oracle, mssql] # subqueries are not permitted in the GROUP BY clause
     - uri: /course{department.code,no,credits,count(class)}?credits=max(fork(count(class)).credits)
     - uri: /program{school_code,code,count(student)}?count(student)>avg(fork().count(student))
     - uri: /program{school_code,code,count(student)}?count(student)>avg(fork(school_code).count(student))
       expect: 400
     - uri: /class^{year, season}
                 {*, /top(^.sort(count(enrollment)-),3).course{title}}
-      ifndef: sqlite    # Too slow.
+      unless: sqlite    # Too slow.
     # parsing a command after an infix function call
     - uri: /school:top/:csv
 
     - uri: /(program.sort(count(student))^degree).^
                 {*, count(student)}
     - uri: /program^degree{*, exists(^), count(^)}
-      ifndef: oracle # Oracle cannot handle EXISTS and an aggregate in
+      unless: oracle # Oracle cannot handle EXISTS and an aggregate in
                      # the same SELECT clause.
     - uri: /school{code, count(program), count(distinct(program{degree}))}
     - uri: /school{code, count(program), count(program^degree)}
   # In PostgreSQL, SUM(int) => bigint, SUM(bigint) => decimal,
   # but HTSQL expects an integer (FIXED?)
   - uri: /sum(department.sum(course.credits))
-    ifdef: pgsql
+    if: pgsql
   # Oracle does not permit correlated subqueries in the same frame
   # with aggregates.
   - uri: /{count(school), exists(school)}
-    ifdef: oracle
+    if: oracle
     expect: 409
     ignore: true
   # MS SQL Server does not permit the argument of an aggregate to
   # contain a subquery.
   - uri: /count(school.exists(department))
-    ifdef: mssql
+    if: mssql
     expect: 409
     ignore: true
 

test/input/tutorial.yaml

 #
 
 title: Examples from the Tutorial
-id: tutorial
+suite: tutorial
 tests:
 
 - title: Getting Started

test/output/mssql.yaml

 # It was generated automatically by the `regress` routine.
 #
 
-id: mssql
+suite: mssql
 tests:
-- id: create-mssql
+- suite: createdb
   tests:
-  - py-include: test/sql/demo-load.py
+  - py: test/sql/demo-load.py
     stdout: ''
-- id: test-mssql
+- suite: test
   tests:
   - include: test/input/schema.yaml
     output:
-      id: schema
+      suite: schema
       tests:
-      - id: tables
+      - suite: tables
         tests:
         - uri: /school
           status: 200 OK
                     [program_requirement].[rationale]
              FROM [rd].[program_requirement]
              ORDER BY 1 ASC, 2 ASC, 3 ASC
-      - id: links
+      - suite: links
         tests:
         - uri: /(school?code='art').department
           status: 200 OK
              ORDER BY [program_requirement].[school_code] ASC, [program_requirement].[program_code] ASC, 1 ASC
   - include: test/input/tutorial.yaml
     output:
-      id: tutorial
+      suite: tutorial
       tests:
-      - id: getting-started
+      - suite: getting-started
         tests:
         - uri: /school
           status: 200 OK
             of Arts and Humanities,Foreign Languages,21\r\nSchool of Arts and Humanities,Art
             History,20\r\nSchool of Arts and Humanities,Political Science,19\r\nSchool
             of Art & Design,Studio Art,19\r\n"
-      - id: relating-and-aggregating-data
+      - suite: relating-and-aggregating-data
         tests:
         - uri: /course{department.name, title}
           status: 200 OK
                   INNER JOIN [ad].[department]
                              ON ([school].[code] = [department].[school_code])
              ORDER BY [school].[code] ASC, 1 ASC
-      - id: calculations-&-references
+      - suite: calculations-references
         tests:
         - uri: /school{name, count(department)}? count(department)>3
           status: 200 OK
                                   ON ([department].[code] = [course_2].[department_code])
              WHERE (CAST([course_1].[credits] AS DECIMAL(38)) > [course_2].[avg])
              ORDER BY [department].[code] ASC, 1 ASC, 2 ASC
-      - id: projections
+      - suite: projections
         tests:
         - uri: /program{degree}
           status: 200 OK
              WHERE ([program].[degree] IS NOT NULL)
              GROUP BY [program].[degree]
              ORDER BY 1 ASC
-      - id: logical-expressions
+      - suite: logical-expressions
         tests:
         - uri: /department?name='Economics'
           status: 200 OK
              FROM [ad].[course]
              WHERE (NULLIF([course].[description], '') IS NULL)
              ORDER BY 1 ASC, 2 ASC
-      - id: types-and-functions
+      - suite: types-and-functions
         tests:
         - uri: /department?school_code=null()
           status: 200 OK
              ORDER BY 1 ASC
   - include: test/input/library.yaml
     output:
-      id: library
+      suite: library
       tests:
-      - id: literals
+      - suite: literals
         tests:
         - uri: /{null(), '', 'HTSQL', '%ce%bb%cf%8c%ce%b3%ce%bf%cf%82'}
           status: 200 OK
             While translating:
                 /{datetime('2010-06-05 29:04')}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-      - id: logical-and-comparison-operators
+      - suite: logical-and-comparison-operators
         tests:
         - uri: /{true(), false()}
           status: 200 OK
             While translating:
                 /{switch(1, 1, 'George', 2, false())}
                                ^^^^^^^^
-      - id: numeric-functions-and-operators
+      - suite: numeric-functions-and-operators
         tests:
         - uri: /{integer(null()), integer('60'), integer(60)}
           status: 200 OK
             While translating:
                 /{trunc(271828e-5,2)}
                   ^^^^^^^^^^^^^^^^^^
-      - id: text-functions-and-operators
+      - suite: text-functions-and-operators
         tests:
         - uri: /{text(null()), text('OMGWTFBBQ')}
           status: 200 OK
              SELECT REPLACE(NULL, 'WTF', 'LOL'),
                     REPLACE('OMGWTFBBQ', '', 'LOL'),
                     REPLACE('OMGWTFBBQ', 'WTF', '')
-      - id: date-functions-and-operators
+      - suite: date-functions-and-operators
         tests:
         - uri: /{date(null()), date('2010-04-15')}
           status: 200 OK
              SELECT (CAST('1991-08-20' AS DATETIME) + 6813),
                     (CAST('2010-04-15' AS DATETIME) - 6813),
                     DATEDIFF(DAY, CAST('1991-08-20' AS DATETIME), CAST('2010-04-15' AS DATETIME))
-      - id: time-functions-and-operators
+      - suite: time-functions-and-operators
         tests:
         - uri: /{time(null()), time('20:03')}
           status: 200 OK
              SELECT DATEPART(HOUR, 0.8424131944444444e0),
                     DATEPART(MINUTE, 0.8424131944444444e0),
                     (DATEPART(SECOND, 0.8424131944444444e0) + DATEPART(MILLISECOND, 0.8424131944444444e0) / 1000e0)
-      - id: datetime-functions-and-operators
+      - suite: datetime-functions-and-operators
         tests:
         - uri: /{datetime(null()), datetime('2010-04-15 20:13')}
           status: 200 OK
              /{datetime('1991-08-20 02:01')+6813.75838544,datetime('2010-04-15 20:13:04.5')-6813.75838544}
              SELECT (CAST('1991-08-20 02:01:00' AS DATETIME) + 6813.75838544e0),
                     (CAST('2010-04-15 20:13:04.500' AS DATETIME) - 6813.75838544e0)
-      - id: aggregate-functions
+      - suite: aggregate-functions
         tests:
         - uri: /course?department_code='lang'
           status: 200 OK
             While translating:
                 /{avg(student.name)}
                   ^^^^^^^^^^^^^^^^^
-      - id: table-functions-and-operators
+      - suite: table-functions-and-operators
         tests:
         - uri: /school.program
           status: 200 OK
                   LEFT OUTER JOIN [ad].[school]
                                   ON ([department].[school_code] = [school].[code])
              ORDER BY 4 ASC
-      - id: decorators
+      - suite: decorators
         tests:
         - uri: /(school :as 'List of Schools')
           status: 200 OK
              ORDER BY 1 DESC, 2 ASC, [course].[department_code] ASC
   - include: test/input/translation.yaml
     output:
-      id: translation
+      suite: translation
       tests:
-      - id: random-collection-of-tests
-        tests:
-        - id: simple-filters
+      - suite: random-collection-of-tests
+        tests:
+        - suite: simple-filters
           tests:
           - uri: /school?code='ns'
             status: 200 OK
                                     ON ([department].[school_code] = [school].[code])
                WHERE ([school].[code] IN ('art', 'la'))
                ORDER BY 1 ASC
-        - id: simple-selectors
+        - suite: simple-selectors
           tests:
           - uri: /school{name}
             status: 200 OK
                     LEFT OUTER JOIN [ad].[school]
                                     ON ([department].[school_code] = [school].[code])
                ORDER BY 4 ASC
-        - id: aggregates
+        - suite: aggregates
           tests:
           - uri: /exists(school)
             status: 200 OK
                                     ON ([school].[code] = [department].[code])
                WHERE ([school].[code] = 'art')
                ORDER BY 1 ASC
-        - id: root,-this,-direct-and-fiber-functions
+        - suite: root-this-direct-and-fiber-functions
           tests:
           - uri: /{count(school)*count(department),count(school.({} -> department))}
             status: 200 OK
                                     ON (1 <> 0)
                WHERE (CAST(COALESCE([department].[count], 0) AS DECIMAL(38)) > [school_2].[avg])
                ORDER BY 1 ASC
-        - id: link-and-fork
+        - suite: link-and-fork
           tests:
           - uri: /school.moniker(department)
             status: 200 OK
                     CROSS JOIN [ad].[department]
                     CROSS JOIN [ad].[program]
                ORDER BY [school].[code] ASC, [department].[code] ASC, 1 ASC, 2 ASC
-        - id: top
+        - suite: top
           tests:
           - uri: /school :top
             status: 200 OK
             - [Content-Type, text/csv; charset=UTF-8]
             - [Content-Disposition, attachment; filename="school.csv"]
             body: "code,name,campus\r\nart,School of Art & Design,old\r\n"
-        - id: identity
+        - suite: identity
           tests:
           - uri: /school[ns]
             status: 200 OK
               While translating:
                   /course[$id]{id(), title} :given $id:='astro.105'
                          ^^^^^
-        - id: table-expressions
+        - suite: table-expressions
           tests:
           - uri: /(school?code='art').department
             status: 200 OK
             - [Content-Type, text/plain; charset=UTF-8]
             - [Vary, Accept]
             body: ''
-        - id: assignments
+        - suite: assignments
           tests:
           - uri: /school.define(c:=department.course.credits) {code,min(c),max(c),sum(c),avg(c)}?exists(c)
             status: 200 OK
               While translating:
                   /school.guard(!is_null($c), filter(campus=$c)) :given $c:='north'
                                 ^^^^^^^^^^^^
-        - id: projections
+        - suite: projections
           tests:
           - uri: /distinct(program{degree})
             status: 200 OK
               While translating:
                   /{(school^campus).campus}
                                     ^^^^^^
-        - id: nested-segments
+        - suite: nested-segments
           tests:
           - uri: /school{code, /department{name}, /program{title}}
             status: 200 OK
               While translating:
                   /school{code, /root().department}
                                 ^^^^^^^^^^^^^^^^^^
-      - id: known-issues
+      - suite: known-issues
         tests:
         - uri: /school?code={'art','art'}
           status: 200 OK
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   - include: test/input/format.yaml
     output:
-      id: format
+      suite: format
       tests:
-      - id: supported-output-formats
+      - suite: supported-output-formats
         tests:
         - uri: /school/:raw
           status: 200 OK
             While translating:
                 /school/:unknown
                 ^^^^^^^^^^^^^^^^
-      - id: format-selection-by-`accept`
+      - suite: format-selection-by-accept
         tests:
         - uri: /school
           status: 200 OK
           - [Content-Type, text/plain; charset=UTF-8]
           - [Vary, Accept]
           body: ''
-      - id: the-`as`-decorator
+      - suite: the-as-decorator
         tests:
         - uri: /(school :as 'List of Schools') {name :as Name, count(department) :as
             '# of Departments'} /:raw
                                    GROUP BY [department].[school_code]) AS [department]
                                   ON ([school].[code] = [department].[school_code])
              ORDER BY [school].[code] ASC
-      - id: data-types
+      - suite: data-types
         tests:
         - uri: /{null(), 'HTSQL', true(), false(), 60, 2.125, 271828e-5, text(null()),
             text(''), text('OMGWTFBBQ'), date('2010-04-15'), time('20:13:04.5'), datetime('2010-04-15
             </table>
             </body>
             </html>
-      - id: identity
+      - suite: identity
         tests:
         - uri: /enrollment[1010.((mth.101).(2008.fall).001)]{id()} /:raw
           status: 200 OK
                    AND ([class_1].[season] = 'fall')
                    AND ([class_1].[section] = '001')
              ORDER BY 1 ASC, [class_1].[department_code] ASC, [class_1].[course_no] ASC, [class_1].[year] ASC, [class_1].[season] ASC, [class_1].[section] ASC
-      - id: no-rows
+      - suite: no-rows
         tests:
         - uri: /school?false()/:raw
           status: 200 OK
              FROM [ad].[school]
              WHERE (0 <> 0)
              ORDER BY 1 ASC
-      - id: empty-selector
+      - suite: empty-selector
         tests:
         - uri: /{}/:raw
           status: 200 OK
                                   ON ([department].[school_code] = [school_2].[code])
              WHERE (([school_1].[school] IS NULL) OR ([school_2].[campus] = 'north'))
              ORDER BY [department].[code] ASC
-      - id: special-characters
+      - suite: special-characters
         tests:
         - uri: /{'%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10', '%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%7F',
             '%CE%BE', '\/%25''"&<>#', ''}/:raw
             | \"\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\x7F\"
             | \u03BE   | \\/%'\"&<>#      | \"\" |\n\n ----\n /{'%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10','%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%7F','\u03BE','\\/%25''\"&<>#',''}\n
             SELECT 1\n"
-      - id: nested-segments
+      - suite: nested-segments
         tests:
         - uri: /{/null, /school.limit(3), /department.limit(5)}/:raw
           status: 200 OK
 
                      SELECT 1
                      WHERE (1 <> 0)
-      - id: scalar-data
+      - suite: scalar-data
         tests:
         - uri: /fetch(null)/:raw
           status: 200 OK
                                     ON ([department].[school_code] = [school].[code])
                WHERE ([school].[campus] = 'old')
                ORDER BY 1 ASC
-      - id: commands
+      - suite: commands
         tests:
         - uri: /school/:fetch
           status: 200 OK
                          ^^^^
   - include: test/input/addon.yaml
     output:
-      id: addon
+      suite: addon
       tests:
-      - id: tweak
+      - suite: tweak
         tests:
         - ctl: [ext, tweak]
           stdout: |+
             TWEAK - contain various tweaks for HTSQL
 
-          exit: 0
-      - id: tweak.autolimit
+      - suite: tweak.autolimit
         tests:
         - ctl: [ext, tweak.autolimit]
           stdout: |+
             Parameters:
               limit=LIMIT              : max. number of rows (default: 10000)
 
-          exit: 0
         - uri: /{2+2}
           status: 200 OK
           headers:
                     [school].[campus]
              FROM [ad].[school]
              ORDER BY 1 ASC
-      - id: tweak.cors
+      - suite: tweak.cors
         tests:
         - ctl: [ext, tweak.cors]
           stdout: |+
             Parameters:
               origin=ORIGIN            : URI that may access the server (default: *)
 
-          exit: 0
         - uri: /school
           status: 200 OK
           headers:
                     [school].[campus]
              FROM [ad].[school]
              ORDER BY 1 ASC
-      - id: tweak.csrf
+      - suite: tweak.csrf
         tests:
         - ctl: [ext, tweak.csrf]
           stdout: |+
               allow-cs-read=ALLOW-CS-READ : permit cross-site read requests
               allow-cs-write=ALLOW-CS-WRITE : permit cross-site write requests
 
-          exit: 0
         - uri: /
           status: 200 OK
           headers:
                     [school].[campus]
              FROM [ad].[school]
              ORDER BY 1 ASC
-      - id: tweak.hello
+      - suite: tweak.hello
         tests:
         - ctl: [ext, tweak.hello]
           stdout: |+
               repeat=REPEAT
               address=ADDRESS
 
-          exit: 0
         - uri: /hello()
           status: 200 OK
           headers:
             Hello, Home!
             Hello, Home!
             Hello, Home!
-      - id: tweak.meta
+      - suite: tweak.meta
         tests:
         - ctl: [ext, tweak.meta]
           stdout: |+
 
                 /table/:meta
 
-          exit: 0
         - uri: /table/:meta
           status: 200 OK
           headers:
             While processing:
                 /meta(/table)
                       ^
-      - id: tweak.override
+      - suite: tweak.override
         tests:
         - ctl: [ext, tweak.override]
           stdout: |+
               unlabeled-columns=COLUMNS : ignored columns
               globals=LABELS           : global definitions
 
-          exit: 0
         - uri: /avg(school.count(program))
           status: 200 OK
           headers:
              WHERE (DATEADD(DAY, 1 - 1, DATEADD(MONTH, 1 - 1, DATEADD(YEAR, DATEPART(YEAR, [student].[dob]) - 2001, CAST('2001-01-01' AS DATETIME)))) IS NOT NULL)
              GROUP BY DATEADD(DAY, 1 - 1, DATEADD(MONTH, 1 - 1, DATEADD(YEAR, DATEPART(YEAR, [student].[dob]) - 2001, CAST('2001-01-01' AS DATETIME))))
              ORDER BY 1 ASC
-      - id: tweak.pool
+      - suite: tweak.pool
         tests:
         - ctl: [ext, tweak.pool]
           stdout: |+
             This addon caches database connections so that a single
             connection could be used to execute more than one query.
 
-          exit: 0
-      - id: tweak.resource
+      - suite: tweak.resource
         tests:
         - ctl: [ext, tweak.resource]
           stdout: |+
             Parameters:
               indicator=STR            : location for static files (default: `-`)
 
-          exit: 0
         - uri: /-/not-found
           status: 404 Not Found
           headers:
           - [Content-Type, text/plain]
           body: |
             Resourse does not exist: '/not-found'.
-      - id: tweak.shell
+      - suite: tweak.shell
         tests:
         - ctl: [ext, tweak.shell]
           stdout: |+
               server-root=URL          : root of HTSQL server
               limit=LIMIT              : max. number of output rows (default: 1000)
 
-          exit: 0
         - ctl: [ext, tweak.shell.default]
           stdout: |+
             TWEAK.SHELL.DEFAULT - make `/shell()` the default output format
               on-default=TRUE|FALSE    : invoke on a query without format
               on-error=TRUE|FALSE      : invoke on errors
 
-          exit: 0
         - uri: /shell()
           status: 200 OK
           headers:
 
       - py: has-sqlalchemy
         stdout: ''
-      - id: tweak.sqlalchemy
+      - suite: tweak.sqlalchemy
         tests:
         - ctl: [ext, tweak.sqlalchemy]
           stdout: |+
               engine=MODULE.NAME       : the SQLAlchemy `engine` object
               metadata=MODULE.NAME     : the SQLAlchemy `metadata` object
 
-          exit: 0
         - py: add-module-path
           stdout: ''
         - uri: /school{code, name, campus}?code='art'
           stdout: ''
   - include: test/input/error.yaml
     output:
-      id: error
+      suite: error
       tests:
-      - id: scan-errors
+      - suite: scan-errors
         tests:
         - uri: /'?%@$'
           status: 400 Bad Request
             While parsing:
                 /`Hello'
                  ^
-      - id: parse-errors
+      - suite: parse-errors
         tests:
         - uri: /'Hello','World'
           status: 400 Bad Request
             While parsing:
                 /{count(school))
                                ^
-      - id: bind-errors
+      - suite: bind-errors
         tests:
         - uri: /department{school, code}
           status: 400 Bad Request
             While translating:
                 /program.program
                          ^^^^^^^
-      - id: lookup-hints
+      - suite: lookup-hints
         tests:
         - uri: /school?code=$scool_code :given $school_code:='art'
           status: 400 Bad Request
             While translating:
                 /program{code, count(student_by_year(2010))}
                                      ^^^^^^^^^^^^^^^^^^^^^
-      - id: function-bind-errors
+      - suite: function-bind-errors
         tests:
         - uri: /count()
           status: 400 Bad Request
             While translating:
                 /max(school{code,name})
                            ^^^^^^^^^^^
-      - id: encode-errors
+      - suite: encode-errors
         tests:
         - uri: /true().fork()
           status: 400 Bad Request
             While translating:
                 /school{code, count(program|department)}
                               ^^^^^^^^^^^^^^^^^^^^^^^^^
-      - id: compile-errors
+      - suite: compile-errors
         tests:
         - uri: /school{code, department.code}
           status: 400 Bad Request
             While translating:
                 /school{department^count(course){*}}
                                                 ^^^
-      - id: serialize-errors
+      - suite: serialize-errors