Repository: frictionlessdata/goodtables-py

Issue:
Add warnings system?
# Overview

It's up on the air related to issues:
- #157
- https://github.com/frictionlessdata/data-quality-spec/issues/11

It should replace dataset level errors and provide ability to do other types of feedback to users inside the goodtables report (not failing with an exception).

It should be like (a `report` part example):

```json
{
  "warninigs": [
    "Invalid datapackage property #1",
    "Invalid datapackage property #2",
    "File <name> inspection has stopped because of error limit",
  ]
}
```

# Tasks

- [ ] implement warning system instead of dataset level errors
- [ ] make presets emit warnings instead of errors
- [ ] add warnings about hitting limits (table, error etc)

Discussion:
roll: @amercader 
please take a look
roll: @amercader 
:+1: 

Patch:
diff --git a/README.md b/README.md
index 2c67352..ea9fe70 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ print(inspector.inspect('data/invalid.csv'))
 # 'valid': False',
 # 'error-count': 2,
 # 'table-count': 1,
-# 'errors': [],
+# 'warnings': [],
 # 'tables': [
 #    {'time': 0.027,
 #     'valid': False',
@@ -90,15 +90,13 @@ As a result of inspection goodtables returns a report dictionary. It includes va
 
 Report errors are categorized by type:
 
-- general - data can't be loaded or parsed
+- source - data can't be loaded or parsed
 - structure - general tabular errors like duplicate headers
 - schema - error of checks against JSON Table Schema
 
 Report errors are categorized by context:
 
-- any - generic errors like IO, HTTP error
-- dataset - the whole dataset errors like invalid datapackage
-- table - the whole table errors like bad encoding
+- table - the whole table errors like IO, HTTP or encoding error
 - head - headers errors
 - body - contents errors
 
@@ -115,7 +113,7 @@ inspector = Inspector()
 inspector.inspect('datapackage.json', preset='datapackage')
 ```
 
-A preset function proceses passed source and options and fills tables list for the following inspection. If any errors have happened a preset function should add them to errors list.
+A preset function proceses passed source and options and fills tables list for the following inspection. If any issues have happened a preset function should add them to warnings list.
 
 #### Builtin presets
 
@@ -138,6 +136,8 @@ from goodtables import Inspector, preset
 
 @preset('custom-preset')
 def custom_preset(source, **options):
+    warnings = []
+    tables = []
     for table in source:
         try:
             tables.append({
@@ -147,12 +147,8 @@ def custom_preset(source, **options):
                 'extra': {...},
             })
         except Exception:
-            errors.append({
-                'code': 'error-code',
-                'message': 'Error message',
-                'row-number': None,
-                'column-number': None,
-            })
+            warnings.append('Warning message')
+    return warnings, tables
 
 inspector = Inspector(custom_presets=[custom_preset])
 inspector.inspect(source, preset='custom-preset')
diff --git a/data/invalid_json.json b/data/invalid_json.json
new file mode 100644
index 0000000..2e8edeb
--- /dev/null
+++ b/data/invalid_json.json
@@ -0,0 +1,3 @@
+{
+    'name': 'name',
+}
diff --git a/data/mixed_datapackage.json b/data/mixed_datapackage.json
new file mode 100644
index 0000000..f9df389
--- /dev/null
+++ b/data/mixed_datapackage.json
@@ -0,0 +1,37 @@
+{
+    "name": "non-tabular",
+    "resources": [
+        {
+            "name": "data",
+            "path": "datapackages/valid/data.csv",
+            "schema": {
+                "fields": [
+                    {
+                        "name": "id",
+                        "type": "string",
+                        "constraints": {
+                            "required": true
+                        }
+                    },
+                    {
+                        "name": "name",
+                        "type": "string"
+                    },
+                    {
+                        "name": "description",
+                        "type": "string"
+                    },
+                    {
+                        "name": "amount",
+                        "type": "number"
+                    }
+                ],
+                "primaryKey": "id"
+            }
+        },
+        {
+            "name": "data2",
+            "path": "datapackages/valid/data2.csv"
+        }
+    ]
+}
diff --git a/data/non_tabular_datapackage.json b/data/non_tabular_datapackage.json
new file mode 100644
index 0000000..62601b0
--- /dev/null
+++ b/data/non_tabular_datapackage.json
@@ -0,0 +1,13 @@
+{
+    "name": "non-tabular",
+    "resources": [
+        {
+            "name": "data",
+            "path": "datapackages/valid/data.csv"
+        },
+        {
+            "name": "data2",
+            "path": "datapackages/valid/data2.csv"
+        }
+    ]
+}
diff --git a/examples/ckan.py b/examples/ckan.py
index 625c6ed..6bb995d 100644
--- a/examples/ckan.py
+++ b/examples/ckan.py
@@ -5,7 +5,7 @@ from goodtables import Inspector, preset
 
 @preset('ckan')
 def ckan_preset(source, **options):
-    errors = []
+    warnings = []
     tables = []
     url = '%s/api/3/action/package_search' % source
     data = requests.get(url).json()
@@ -22,7 +22,7 @@ def ckan_preset(source, **options):
                         'publisher': package['organization']['name']
                     },
                 })
-    return errors, tables
+    return warnings, tables
 
 inspector = Inspector(custom_presets=[ckan_preset])
 report = inspector.inspect('http://data.surrey.ca', preset='ckan')
diff --git a/examples/custom_preset.py b/examples/custom_preset.py
index a8c1151..9f286dd 100644
--- a/examples/custom_preset.py
+++ b/examples/custom_preset.py
@@ -6,7 +6,7 @@ from goodtables import Inspector, preset
 
 @preset('csvdir')
 def csvdir(source):
-    errors = []
+    warnings = []
     tables = []
     for name in os.listdir(source):
         path = os.path.join(source, name)
@@ -19,7 +19,7 @@ def csvdir(source):
                     'filename': name,
                 },
             })
-    return errors, tables
+    return warnings, tables
 
 
 inspector = Inspector(custom_presets=[csvdir])
diff --git a/examples/dropbox.py b/examples/dropbox.py
index 00a5d27..d3417d3 100644
--- a/examples/dropbox.py
+++ b/examples/dropbox.py
@@ -13,7 +13,7 @@ client = dropbox.dropbox.Dropbox(ACCESS_TOKEN)
 
 @preset('dropbox')
 def dropbox_preset(source, **options):
-    errors = []
+    warnings = []
     tables = []
     for item in client.files_list_folder(source).entries:
         if item.path_lower.endswith('.csv'):
@@ -26,7 +26,7 @@ def dropbox_preset(source, **options):
                     'folder': source,
                 },
             })
-    return errors, tables
+    return warnings, tables
 
 inspector = Inspector(custom_presets=[dropbox_preset])
 report = inspector.inspect(FOLDER, preset='dropbox')
diff --git a/features/fail_fast_two_schema_errors.yml b/features/fail_fast_two_schema_errors.yml
index 8129dcc..1b1aed1 100644
--- a/features/fail_fast_two_schema_errors.yml
+++ b/features/fail_fast_two_schema_errors.yml
@@ -5,6 +5,6 @@ fail_fast_two_schema_errors:
   source: data/fail_fast_two_schema_errors.csv
   schema: data/test_schema.json
   report:
-    - [1, 4, 3, 'required-constraint']
     - [1, 4, 1, 'non-castable-value']
+    - [1, 4, 3, 'required-constraint']
     - [1, 5, 1, 'required-constraint']
diff --git a/goodtables/checks/body/duplicate_row.py b/goodtables/checks/body/duplicate_row.py
index 407f483..8ab2e37 100644
--- a/goodtables/checks/body/duplicate_row.py
+++ b/goodtables/checks/body/duplicate_row.py
@@ -14,20 +14,24 @@ from ...register import check
 @check('duplicate-row')
 def duplicate_row(errors, columns, row_number, state):
     rindex = state.setdefault('rindex', {})
-    pointer = hash(json.dumps(list(column.get('value') for column in columns)))
-    references = rindex.setdefault(pointer, [])
-    if references:
-        # Add error
-        message = spec['errors']['duplicate-row']['message']
-        message = message.format(
-            row_number=row_number,
-            row_numbers=', '.join(map(str, references)))
-        errors.append({
-            'code': 'duplicate-row',
-            'message': message,
-            'row-number': row_number,
-            'column-number': None,
-        })
-        # Clear columns
-        del columns[:]
-    references.append(row_number)
+    try:
+        pointer = hash(json.dumps(list(column.get('value') for column in columns)))
+        references = rindex.setdefault(pointer, [])
+    except TypeError:
+        pointer = None
+    if pointer:
+        if references:
+            # Add error
+            message = spec['errors']['duplicate-row']['message']
+            message = message.format(
+                row_number=row_number,
+                row_numbers=', '.join(map(str, references)))
+            errors.append({
+                'code': 'duplicate-row',
+                'message': message,
+                'row-number': row_number,
+                'column-number': None,
+            })
+            # Clear columns
+            del columns[:]
+        references.append(row_number)
diff --git a/goodtables/cli.py b/goodtables/cli.py
index f19385f..42ad027 100644
--- a/goodtables/cli.py
+++ b/goodtables/cli.py
@@ -55,17 +55,14 @@ def _print_report(report, json=False):
         return print(json_module.dumps(report, indent=4))
     color = 'green' if report['valid'] else 'red'
     tables = report.pop('tables')
-    errors = report.pop('errors')
+    warnings = report.pop('warnings')
     click.secho('DATASET', bold=True)
     click.secho('='*7, bold=True)
     click.secho(pformat(report), fg=color, bold=True)
-    if errors:
+    if warnings:
         click.secho('-'*9, bold=True)
-    for error in errors:
-        error = {key: value or '-' for key, value in error.items()}
-        template = '[{row-number},{column-number}] [{code}] {message}'
-        message = template.format(**error)
-        click.secho(message)
+    for warning in warnings:
+        click.secho('Warning: %s' % warning, fg='yellow')
     for table_number, table in enumerate(tables, start=1):
         click.secho('\nTABLE [%s]' % table_number, bold=True)
         click.secho('='*9, bold=True)
diff --git a/goodtables/inspector.py b/goodtables/inspector.py
index ec85860..dce6a06 100644
--- a/goodtables/inspector.py
+++ b/goodtables/inspector.py
@@ -54,10 +54,9 @@ class Inspector(object):
         self.__table_limit = table_limit
         self.__row_limit = row_limit
         self.__infer_schema = infer_schema
-        self.__infer_fields = infer_fields
-        self.__order_fields = order_fields
-        self.__presets = self.__prepare_presets(copy(custom_presets))
-        self.__checks = self.__prepare_checks(checks, copy(custom_checks))
+        self.__presets = _prepare_presets(copy(custom_presets))
+        self.__checks = _prepare_checks(checks, copy(custom_checks),
+            order_fields=order_fields, infer_fields=infer_fields)
 
     def inspect(self, source, preset='table', **options):
         """Inspect source with given preset and options.
@@ -90,34 +89,36 @@ class Inspector(object):
             raise exceptions.GoodtablesException(message)
 
         # Prepare tables
-        errors, tables = preset_func(source, **options)
-        tables = tables[:self.__table_limit]
-        for error in errors:
-            error['row'] = None
-
-        # Collect reports
-        reports = []
+        warnings, tables = preset_func(source, **options)
+        if len(tables) > self.__table_limit:
+            warnings.append(
+                'Dataset inspection has reached %s table(s) limit' %
+                (self.__table_limit))
+            tables = tables[:self.__table_limit]
+
+        # Collect table reports
+        table_reports = []
         if tables:
             tasks = []
             pool = ThreadPool(processes=len(tables))
             for table in tables:
                 tasks.append(pool.apply_async(self.__inspect_table, (table,)))
             for task in tasks:
-                report = task.get()
-                reports.append(report)
+                table_warnings, table_report = task.get()
+                warnings.extend(table_warnings)
+                table_reports.append(table_report)
 
         # Stop timer
         stop = datetime.datetime.now()
 
         # Compose report
-        errors = errors[:self.__error_limit]
         report = {
             'time': round((stop - start).total_seconds(), 3),
-            'valid': not bool(errors) and all(report['valid'] for report in reports),
-            'error-count': len(errors) + sum(len(report['errors']) for report in reports),
+            'valid': all(item['valid'] for item in table_reports),
+            'error-count': sum(len(item['errors']) for item in table_reports),
             'table-count': len(tables),
-            'tables': reports,
-            'errors': errors,
+            'tables': table_reports,
+            'warnings': warnings,
         }
 
         return report
@@ -131,6 +132,7 @@ class Inspector(object):
 
         # Prepare vars
         errors = []
+        warnings = []
         headers = None
         row_number = 0
         fatal_error = False
@@ -145,14 +147,14 @@ class Inspector(object):
             stream.open()
             sample = stream.sample
             headers = stream.headers
-            if self.__filter_checks(checks, type='schema'):
+            if _filter_checks(checks, type='schema'):
                 if schema is None and self.__infer_schema:
                     schema = Schema(infer(headers, sample))
             if schema is None:
-                checks = self.__filter_checks(checks, type='schema', inverse=True)
+                checks = _filter_checks(checks, type='schema', inverse=True)
         except Exception as exception:
             fatal_error = True
-            error = self.__compose_error_from_exception(exception)
+            error = _compose_error_from_exception(exception)
             errors.append(error)
 
         # Prepare columns
@@ -172,7 +174,7 @@ class Inspector(object):
 
         # Head checks
         if not fatal_error:
-            head_checks = self.__filter_checks(checks, context='head')
+            head_checks = _filter_checks(checks, context='head')
             for check in head_checks:
                 if not columns:
                     break
@@ -184,7 +186,7 @@ class Inspector(object):
         if not fatal_error:
             states = {}
             colmap = {column['number']: column for column in columns}
-            body_checks = self.__filter_checks(checks, context='body')
+            body_checks = _filter_checks(checks, context='body')
             with stream:
                 extended_rows = stream.iter(extended=True)
                 while True:
@@ -194,7 +196,7 @@ class Inspector(object):
                         break
                     except Exception as exception:
                         fatal_error = True
-                        error = self.__compose_error_from_exception(exception)
+                        error = _compose_error_from_exception(exception)
                         errors.append(error)
                         break
                     columns = []
@@ -219,8 +221,14 @@ class Inspector(object):
                             break
                         error['row'] = row
                     if row_number >= self.__row_limit:
+                        warnings.append(
+                            'Table "%s" inspection has reached %s row(s) limit' %
+                            (source, self.__row_limit))
                         break
                     if len(errors) >= self.__error_limit:
+                        warnings.append(
+                            'Table "%s" inspection has reached %s error(s) limit' %
+                            (source, self.__error_limit))
                         break
 
         # Stop timer
@@ -228,6 +236,7 @@ class Inspector(object):
 
         # Compose report
         errors = errors[:self.__error_limit]
+        errors = _sort_errors(errors)
         report = copy(extra)
         report.update({
             'time': round((stop - start).total_seconds(), 3),
@@ -239,120 +248,127 @@ class Inspector(object):
             'errors': errors,
         })
 
-        return report
-
-    def __prepare_presets(self, custom):
+        return warnings, report
 
-        # Prepare presets
-        presets = {}
-        for preset in chain(vars(presets_module).values(), custom):
-            descriptor = getattr(preset, 'preset', None)
-            if descriptor:
-                presets[descriptor['name']] = preset
 
-        return presets
+# Internal
 
-    def __prepare_checks(self, setup, custom):
+_FILLVALUE = '_FILLVALUE'
 
-        # Prepare errors/checkmap
-        errors = []
-        checkmap = {}
-        for code in config.CHECKS:
-            error = copy(spec['errors'][code])
-            error.update({'code': code})
-            errors.append(error)
-        for check in chain(vars(checks_module).values(), custom):
-            desc = getattr(check, 'check', None)
-            if desc:
-                errormap = {error['code']: index for index, error in enumerate(errors)}
-                if desc['before'] in errormap:
-                    errors.insert(errormap[desc['before']], desc)
-                if desc['after'] in errormap:
-                    errors.insert(errormap[desc['after']] + 1, desc)
-                checkmap[desc['code']] = check
-
-        # Prepare checks
-        checks = []
-        for error in errors:
-            if error['code'] in checkmap:
-                checks.append({
-                    'func': checkmap[error['code']],
-                    'code': error['code'],
-                    'type': error['type'],
-                    'context': error['context'],
-                })
-
-        # Filter structure checks
-        if setup == 'structure':
-            checks = self.__filter_checks(checks, type='structure')
-
-        # Filter schema checks
-        elif setup == 'schema':
-            checks = self.__filter_checks(checks, type='schema')
-
-        # Filter granular checks
-        elif isinstance(setup, dict):
-            default = True not in setup.values()
-            checks = [check for check in checks
-                if setup.get(check['code'], default)]
-
-        # Unknown filter
-        elif setup != 'all':
-            message = 'Checks filter "%s" is not supported' % setup
-            raise exceptions.GoodtablesException(message)
 
-        # Bind options
-        for check in checks:
-            args, _, _, _ = inspect.getargspec(check['func'])
-            if 'order_fields' in args:
-                check['func'] = partial(check['func'],
-                    order_fields=self.__order_fields)
-            if 'infer_fields' in args:
-                check['func'] = partial(check['func'],
-                    infer_fields=self.__infer_fields)
-
-        return checks
-
-    def __filter_checks(self, checks, type=None, context=None, inverse=False):
-
-        # Filted checks
-        result = []
-        comparator = operator.ne
-        if inverse:
-            comparator = operator.eq
-        for check in checks:
-            if type and comparator(check['type'], type):
-                continue
-            if context and comparator(check['context'], context):
-                continue
-            result.append(check)
-
-        return result
-
-    def __compose_error_from_exception(self, exception):
+def _prepare_presets(custom):
+
+    # Prepare presets
+    presets = {}
+    for preset in chain(vars(presets_module).values(), custom):
+        descriptor = getattr(preset, 'preset', None)
+        if descriptor:
+            presets[descriptor['name']] = preset
+
+    return presets
+
+
+def _prepare_checks(setup, custom, order_fields, infer_fields):
+
+    # Prepare errors/checkmap
+    errors = []
+    checkmap = {}
+    for code in config.CHECKS:
+        error = copy(spec['errors'][code])
+        error.update({'code': code})
+        errors.append(error)
+    for check in chain(vars(checks_module).values(), custom):
+        desc = getattr(check, 'check', None)
+        if desc:
+            errormap = {error['code']: index for index, error in enumerate(errors)}
+            if desc['before'] in errormap:
+                errors.insert(errormap[desc['before']], desc)
+            if desc['after'] in errormap:
+                errors.insert(errormap[desc['after']] + 1, desc)
+            checkmap[desc['code']] = check
+
+    # Prepare checks
+    checks = []
+    for error in errors:
+        if error['code'] in checkmap:
+            checks.append({
+                'func': checkmap[error['code']],
+                'code': error['code'],
+                'type': error['type'],
+                'context': error['context'],
+            })
+
+    # Filter structure checks
+    if setup == 'structure':
+        checks = _filter_checks(checks, type='structure')
+
+    # Filter schema checks
+    elif setup == 'schema':
+        checks = _filter_checks(checks, type='schema')
+
+    # Filter granular checks
+    elif isinstance(setup, dict):
+        default = True not in setup.values()
+        checks = [check for check in checks
+            if setup.get(check['code'], default)]
+
+    # Unknown filter
+    elif setup != 'all':
+        message = 'Checks filter "%s" is not supported' % setup
+        raise exceptions.GoodtablesException(message)
+
+    # Bind options
+    for check in checks:
+        args, _, _, _ = inspect.getargspec(check['func'])
+        if 'order_fields' in args:
+            check['func'] = partial(check['func'], order_fields=order_fields)
+        if 'infer_fields' in args:
+            check['func'] = partial(check['func'], infer_fields=infer_fields)
+
+    return checks
+
+
+def _filter_checks(checks, type=None, context=None, inverse=False):
+
+    # Filted checks
+    result = []
+    comparator = operator.ne
+    if inverse:
+        comparator = operator.eq
+    for check in checks:
+        if type and comparator(check['type'], type):
+            continue
+        if context and comparator(check['context'], context):
+            continue
+        result.append(check)
+
+    return result
+
+
+def _compose_error_from_exception(exception):
+    code = 'source-error'
+    message = str(exception)
+    if isinstance(exception, tabulator.exceptions.SourceError):
         code = 'source-error'
-        message = str(exception)
-        if isinstance(exception, tabulator.exceptions.SourceError):
-            code = 'source-error'
-        elif isinstance(exception, tabulator.exceptions.SchemeError):
-            code = 'scheme-error'
-        elif isinstance(exception, tabulator.exceptions.FormatError):
-            code = 'format-error'
-        elif isinstance(exception, tabulator.exceptions.EncodingError):
-            code = 'encoding-error'
-        elif isinstance(exception, tabulator.exceptions.IOError):
-            code = 'io-error'
-        elif isinstance(exception, tabulator.exceptions.HTTPError):
-            code = 'http-error'
-        return {
-            'row': None,
-            'code': code,
-            'message': message,
-            'row-number': None,
-            'column-number': None,
-        }
-
-
-# Internal
-
-_FILLVALUE = '_FILLVALUE'
+    elif isinstance(exception, tabulator.exceptions.SchemeError):
+        code = 'scheme-error'
+    elif isinstance(exception, tabulator.exceptions.FormatError):
+        code = 'format-error'
+    elif isinstance(exception, tabulator.exceptions.EncodingError):
+        code = 'encoding-error'
+    elif isinstance(exception, tabulator.exceptions.IOError):
+        code = 'io-error'
+    elif isinstance(exception, tabulator.exceptions.HTTPError):
+        code = 'http-error'
+    return {
+        'row': None,
+        'code': code,
+        'message': message,
+        'row-number': None,
+        'column-number': None,
+    }
+
+
+def _sort_errors(errors):
+    return sorted(errors, key=lambda error:
+        (error['row-number'] or 0, error['column-number']))
diff --git a/goodtables/presets/datapackage.py b/goodtables/presets/datapackage.py
index 5d67c14..b956bc5 100644
--- a/goodtables/presets/datapackage.py
+++ b/goodtables/presets/datapackage.py
@@ -6,45 +6,56 @@ from __future__ import unicode_literals
 
 from tabulator import Stream
 from jsontableschema import Schema
+from jsontableschema.exceptions import SchemaValidationError
 from datapackage import DataPackage
 from ..register import preset
-from ..spec import spec
 
 
 # Module API
 
 @preset('datapackage')
 def datapackage(source, **options):
-    errors = []
+    warnings = []
     tables = []
 
-    # Prepare datapackage
-    datapackage = DataPackage(source, **options)
-    for exception in datapackage.iter_errors():
-        # Error message should contain datapackage source (often it's path)
-        message = spec['errors']['datapackage-error']['message']
-        message = message.format(
-            error_message='{problem} [{source}]'.format(
-                problem=str(exception).splitlines()[0],
-                source=str(source)))
-        errors.append({
-            'code': 'datapackage-error',
-            'message': message,
-            'row-number': None,
-            'column-number': None,
-        })
-
-    # Add tables
-    if not errors:
+    # Load datapackage
+    try:
+        datapackage = DataPackage(source, **options)
+    except Exception as error:
+        warnings.append(
+            'Data Package "%s" has a loading error "%s"' %
+            (source, error))
+
+    # Validate datapackage
+    if not warnings:
+        for error in datapackage.iter_errors():
+            warnings.append(
+                'Data Package "%s" has a validation error "%s"' %
+                (source, str(error).splitlines()[0]))
+
+    # Extract datapackage tables
+    if not warnings:
         for resource in datapackage.resources:
+            # TODO: after datapackage-v1 will be ready
+            # - we should use `resource.tabular` to filter tabular resources
+            # - we don't need to validate schema here because of dereferencing
             path = resource.remote_data_path or resource.local_data_path
+            try:
+                schema = Schema(resource.descriptor['schema'])
+            except SchemaValidationError as error:
+                warnings.append(
+                    'Data Package "%s" has a validation error "%s"' %
+                    (source, str(error).splitlines()[0]))
+                continue
+            except Exception:
+                continue
             tables.append({
                 'source': path,
                 'stream': Stream(path, headers=1),
-                'schema': Schema(resource.descriptor['schema']),
+                'schema': schema,
                 'extra': {
                     'datapackage': str(source),
                 },
             })
 
-    return errors, tables
+    return warnings, tables
diff --git a/goodtables/presets/nested.py b/goodtables/presets/nested.py
index fa63a8d..7a55409 100644
--- a/goodtables/presets/nested.py
+++ b/goodtables/presets/nested.py
@@ -13,10 +13,10 @@ from .. import exceptions
 
 @preset('nested')
 def nested(source, presets):
-    errors = []
+    warnings = []
     tables = []
 
-    # Add errors, tables
+    # Add warnings, tables
     source = deepcopy(source)
     for item in source:
         preset = item.pop('preset', 'table')
@@ -28,8 +28,8 @@ def nested(source, presets):
         except KeyError:
             message = 'Preset "%s" is not registered' % preset
             raise exceptions.GoodtablesException(message)
-        item_errors, item_tables = preset_func(**item)
-        errors.extend(item_errors)
+        item_warnings, item_tables = preset_func(**item)
+        warnings.extend(item_warnings)
         tables.extend(item_tables)
 
-    return errors, tables
+    return warnings, tables
diff --git a/goodtables/presets/table.py b/goodtables/presets/table.py
index f7a2464..a2b53fb 100644
--- a/goodtables/presets/table.py
+++ b/goodtables/presets/table.py
@@ -4,46 +4,44 @@ from __future__ import print_function
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
+import six
 import jsontableschema
 from tabulator import Stream
 from jsontableschema import Schema, validate
 from ..register import preset
-from ..spec import spec
 
 
 # Module API
 
 @preset('table')
 def table(source, schema=None, **options):
-    errors = []
+    warnings = []
     tables = []
 
+    # Ensure not a datapackage
+    if isinstance(source, six.string_types):
+        if source.endswith('datapackage.json'):
+            warnings.append('Use "datapackage" preset for Data Packages')
+
     # Prepare schema
-    if schema is not None:
-        descriptor = schema
-        try:
-            # https://github.com/frictionlessdata/jsontableschema-py/issues/113
-            from jsontableschema.helpers import load_json_source
-            loaded_descriptor = load_json_source(schema)
-            validate(loaded_descriptor, no_fail_fast=True)
-            schema = Schema(loaded_descriptor)
-        except jsontableschema.exceptions.MultipleInvalid as exception:
-            for error in exception.errors:
-                # Error message should contain schema source (often it's path)
-                message = spec['errors']['jsontableschema-error']['message']
-                message = message.format(
-                    error_message='{problem} [{source}]'.format(
-                        problem=str(error).splitlines()[0],
-                        source=str(descriptor)))
-                errors.append({
-                    'code': 'jsontableschema-error',
-                    'message': message,
-                    'row-number': None,
-                    'column-number': None,
-                })
+    if not warnings:
+        if schema is not None:
+            # TODO: after tableschema-v1 will be ready
+            # - we should use Schema(strict=False) to handle schema errors on inspection
+            # - it means we don't need to validate schema here, mess with helpers etc
+            try:
+                from jsontableschema.helpers import load_json_source
+                loaded_descriptor = load_json_source(schema)
+                validate(loaded_descriptor, no_fail_fast=True)
+                schema = Schema(loaded_descriptor)
+            except jsontableschema.exceptions.MultipleInvalid as exception:
+                for error in exception.errors:
+                    warnings.append(
+                        'Table schema "%s" has a validation error "%s"' %
+                        (schema, str(error).splitlines()[0]))
 
     # Add table
-    if not errors:
+    if not warnings:
         options.setdefault('headers', 1)
         tables.append({
             'source': str(source),
@@ -52,4 +50,4 @@ def table(source, schema=None, **options):
             'extra': {},
         })
 
-    return errors, tables
+    return warnings, tables
diff --git a/goodtables/spec.json b/goodtables/spec.json
index b367393..fe3e46b 100644
--- a/goodtables/spec.json
+++ b/goodtables/spec.json
@@ -1,39 +1,23 @@
 {
-    "version": "1.0.0-alpha2",
+    "version": "1.0.0-alpha3",
     "errors": {
 
 
         "io-error": {
           "name": "IO Error",
           "type": "source",
-          "context": "any",
+          "context": "table",
           "weight": 100,
           "message": "The data source returned an IO Error of type {error_type}",
-          "description": ""
+          "description": "Data reading error because of IO error.\n\n How it could be resolved:\n - Fix path if it's not correct."
         },
         "http-error": {
           "name": "HTTP Error",
           "type": "source",
-          "context": "any",
+          "context": "table",
           "weight": 100,
           "message": "The data source returned an HTTP error with a status code of {status_code}",
-          "description": ""
-        },
-        "datapackage-error": {
-          "name": "DataPackage Error",
-          "type": "source",
-          "context": "dataset",
-          "weight": 100,
-          "message": "DataPackage error: {error_message}",
-          "description": ""
-        },
-        "jsontableschema-error": {
-          "name": "JSON Table Schema Error",
-          "type": "source",
-          "context": "dataset",
-          "weight": 100,
-          "message": "JSON Table Schema error: {error_message}",
-          "description": ""
+          "description": "Data reading error because of HTTP error.\n\n How it could be resolved:\n - Fix url link if it's not correct."
         },
         "source-error": {
           "name": "Source Error",
@@ -41,7 +25,7 @@
           "context": "table",
           "weight": 100,
           "message": "The data source has not supported or has inconsistent contents; no tabular data can be extracted",
-          "description": ""
+          "description": "Data reading error because of not supported or inconsistent contents.\n\n How it could be resolved:\n - Fix data contents (e.g. change JSON data to array or arrays/objects).\n - Set correct source settings in {validator}."
         },
         "scheme-error": {
           "name": "Scheme Error",
@@ -49,7 +33,7 @@
           "context": "table",
           "weight": 100,
           "message": "The data source is in an unknown scheme; no tabular data can be extracted",
-          "description": ""
+          "description": "Data reading error because of incorrect scheme.\n\n How it could be resolved:\n - Fix data scheme (e.g. change scheme from `ftp` to `http`).\n - Set correct scheme in {validator}."
         },
         "format-error": {
           "name": "Format Error",
@@ -57,7 +41,7 @@
           "context": "table",
           "weight": 100,
           "message": "The data source is in an unknown format; no tabular data can be extracted",
-          "description": ""
+          "description": "Data reading error because of incorrect format.\n\n How it could be resolved:\n - Fix data format (e.g. change file extension from `txt` to `csv`).\n - Set correct format in {validator}."
         },
         "encoding-error": {
           "name": "Encoding Error",
@@ -65,7 +49,7 @@
           "context": "table",
           "weight": 100,
           "message": "The data source could not be successfully decoded with {encoding} encoding",
-          "description": ""
+          "description": "Data reading error because of an encoding problem.\n\n How it could be resolved:\n - Fix data source if it's broken.\n - Set correct encoding in {validator}."
         },
 
 
@@ -75,7 +59,7 @@
           "context": "head",
           "weight": 3,
           "message": "Header in column {column_number} is blank",
-          "description": ""
+          "description": "A column in the header row is missing a value. Column names should be provided.\n\n How it could be resolved:\n - Add the missing column name to the first row of the data source.\n - If the first row starts with, or ends with a comma, remove it.\n - If this error should be ignored disable `blank-header` check in {validator}."
         },
         "duplicate-header": {
           "name": "Duplicate Header",
@@ -83,7 +67,7 @@
           "context": "head",
           "weight": 3,
           "message": "Header in column {column_number} is duplicated to header in column(s) {column_numbers}",
-          "description": ""
+          "description": "Two columns in the header row have the same value. Column names should be unique.\n\n How it could be resolved:\n - Add the missing column name to the first row of the data.\n - If the first row starts with, or ends with a comma, remove it.\n - If this error should be ignored disable `duplicate-header` check in {validator}."
         },
         "blank-row": {
           "name": "Blank Row",
@@ -91,7 +75,7 @@
           "context": "body",
           "weight": 9,
           "message": "Row {row_number} is completely blank",
-          "description": ""
+          "description": "This row is empty. A row should contain at least one value.\n\n How it could be resolved:\n - Delete the row.\n - If this error should be ignored disable `blank-row` check in {validator}."
         },
         "duplicate-row": {
           "name": "Duplicate Row",
@@ -99,7 +83,7 @@
           "context": "body",
           "weight": 5,
           "message": "Row {row_number} is duplicated to row(s) {row_numbers}",
-          "description": ""
+          "description": "The exact same data has been seen in another row.\n\n How it could be resolved:\n - If some of the data is incorrect, correct it.\n - If the whole row is an incorrect duplicate, remove it.\n - If this error should be ignored disable `duplicate-row` check in {validator}."
         },
         "extra-value": {
           "name": "Extra Value",
@@ -107,7 +91,7 @@
           "context": "body",
           "weight": 9,
           "message": "Row {row_number} has an extra value in column {column_number}",
-          "description": ""
+          "description": "This row has more values compared to the header row (the first row in the data source). A key concept is that all the rows in tabular data must have the same number of columns.\n\n How it could be resolved:\n - Check data has an extra comma between the values in this row.\n - If this error should be ignored disable `extra-value` check in {validator}."
         },
         "missing-value": {
           "name": "Missing Value",
@@ -115,17 +99,25 @@
           "context": "body",
           "weight": 9,
           "message": "Row {row_number} has a missing value in column {column_number}",
-          "description": ""
+          "description": "This row has less values compared to the header row (the first row in the data source). A key concept is that all the rows in tabular data must have the same number of columns.\n\n How it could be resolved:\n - Check data is not missing a comma between the values in this row.\n - If this error should be ignored disable `missing-value` check in {validator}."
         },
 
 
+        "schema-error": {
+          "name": "Table Schema Error",
+          "type": "schema",
+          "context": "table",
+          "weight": 15,
+          "message": "Table Schema error: {error_message}",
+          "description": "Provided schema is not valid.\n\n How it could be resolved:\n - Update schema descriptor to be a valid descriptor\n - If this error should be ignored disable schema cheks in {validator}."
+        },
         "non-matching-header": {
           "name": "Non-Matching Header",
           "type": "schema",
           "context": "head",
           "weight": 9,
           "message": "Header in column {column_number} doesn't match field name {field_name}",
-          "description": ""
+          "description": "One of the data source headers doens't match the field name defined in the schema.\n\n How it could be resolved:\n - Rename header in the data source or field in the schema\n - If this error should be ignored disable `non-matching-header` check in {validator}."
         },
         "extra-header": {
           "name": "Extra Header",
@@ -133,7 +125,7 @@
           "context": "head",
           "weight": 9,
           "message": "There is an extra header in column {column_number}",
-          "description": ""
+          "description": "The first row of the data source contains header that doesn't exist in the schema.\n\n How it could be resolved:\n - Remove the extra column from the data source or add the missing field to the schema\n - If this error should be ignored disable `extra-header` check in {validator}."
         },
         "missing-header": {
           "name": "Missing Header",
@@ -141,7 +133,15 @@
           "context": "head",
           "weight": 9,
           "message": "There is a missing header in column {column_number}",
-          "description": ""
+          "description": "Based on the schema there should be a header that is missing in the first row of the data source.\n\n How it could be resolved:\n - Add the missing column to the data source or remove the extra field from the schema\n - If this error should be ignored disable `missing-header` check in {validator}."
+        },
+        "non-castable-value": {
+          "name": "Non-Castable Value",
+          "type": "schema",
+          "context": "body",
+          "weight": 9,
+          "message": "Row {row_number} has non castable value {value} in column {column_number} (type: {field_type}, format: {field_format})",
+          "description": "The value can't be cast based on the schema type and format for this field.\n\n How it could be resolved:\n - If this value is not correct, update the value.\n - If this error should be ignored disable `non-castable-value` check in {validator}. In this case all schema checks for row values will be ignored."
         },
         "required-constraint": {
           "name": "Required Constraint",
@@ -149,7 +149,7 @@
           "context": "body",
           "weight": 9,
           "message": "Column {column_number} is a required field, but row {row_number} has no value",
-          "description": ""
+          "description": "This field is a required field, but it contains no value.\n\n How it could be resolved:\n - If this value is not correct, update the value.\n - If value is correct, then remove the `required` constraint from the schema.\n - If this error should be ignored disable `required-constraint` check in {validator}."
         },
         "pattern-constraint": {
           "name": "Pattern Constraint",
@@ -157,15 +157,7 @@
           "context": "body",
           "weight": 7,
           "message": "The value {value} in row {row_number} and column {column_number} does not conform to the pattern constraint of {constraint}",
-          "description": ""
-        },
-        "non-castable-value": {
-          "name": "Non-Castable Value",
-          "type": "schema",
-          "context": "body",
-          "weight": 9,
-          "message": "Row {row_number} has non castable value {value} in column {column_number} (type: {field_type}, format: {field_format})",
-          "description": ""
+          "description": "This field value should conform to constraint pattern.\n\n How it could be resolved:\n - If this value is not correct, update the value.\n - If value is correct, then remove the `pattern` constraint from the schema.\n - If this error should be ignored disable `pattern-constraint` check in {validator}."
         },
         "unique-constraint": {
           "name": "Unique Constraint",
@@ -173,7 +165,7 @@
           "context": "body",
           "weight": 9,
           "message": "Rows {row_numbers} has unique constraint violation in column {column_number}",
-          "description": ""
+          "description": "This field is a unique field but it contains a value that has been used in another row.\n\n How it could be resolved:\n - If this value is not correct, update the value.\n - If value is correct, then the values in this column are not unique. Remove the `unique` constraint from the schema.\n - If this error should be ignored disable `unique-constraint` check in {validator}."
         },
         "enumerable-constraint": {
           "name": "Enumerable Constraint",
@@ -181,7 +173,7 @@
           "context": "body",
           "weight": 7,
           "message": "The value {value} in row {row_number} and column {column_number} does not conform to the given enumeration: {constraint}",
-          "description": ""
+          "description": "This field value should be equal to one of the constraint enumeration.\n\n How it could be resolved:\n - If this value is not correct, update the value.\n - If value is correct, then remove the `enum` constraint from the schema.\n - If this error should be ignored disable `enumerable-constraint` check in {validator}."
         },
         "minimum-constraint": {
           "name": "Minimum Constraint",
@@ -189,7 +181,7 @@
           "context": "body",
           "weight": 7,
           "message": "The value {value} in row {row_number} and column {column_number} does not conform to the minimum constraint of {constraint}",
-          "description": ""
+          "description": "This field value should be greater or equal than constraint value.\n\n How it could be resolved:\n - If this value is not correct, update the value.\n - If value is correct, then remove the `minimum` constraint from the schema.\n - If this error should be ignored disable `minimum-constraint` check in {validator}."
         },
         "maximum-constraint": {
           "name": "Maximum Constraint",
@@ -197,7 +189,7 @@
           "context": "body",
           "weight": 7,
           "message": "The value {value} in row {row_number} and column {column_number} does not conform to the maximum constraint of {constraint}",
-          "description": ""
+          "description": "This field value should be less or equal than constraint value.\n\n How it could be resolved:\n - If this value is not correct, update the value.\n - If value is correct, then remove the `maximum` constraint from the schema.\n - If this error should be ignored disable `maximum-constraint` check in {validator}."
         },
         "minimum-length-constraint": {
           "name": "Minimum Length Constraint",
@@ -205,7 +197,7 @@
           "context": "body",
           "weight": 7,
           "message": "The value {value} in row {row_number} and column {column_number} does not conform to the minimum length constraint of {constraint}",
-          "description": ""
+          "description": "A lenght of this field value should be greater or equal than schema constraint value.\n\n How it could be resolved:\n - If this value is not correct, update the value.\n - If value is correct, then remove the `minimumLength` constraint from the schema.\n - If this error should be ignored disable `minimum-length-constraint` check in {validator}."
         },
         "maximum-length-constraint": {
           "name": "Maximum Length Constraint",
@@ -213,7 +205,7 @@
           "context": "body",
           "weight": 7,
           "message": "The value {value} in row {row_number} and column {column_number} does not conform to the maximum length constraint of {constraint}",
-          "description": ""
+          "description": "A lenght of this field value should be less or equal than schema constraint value.\n\n How it could be resolved:\n - If this value is not correct, update the value.\n - If value is correct, then remove the `maximumLength` constraint from the schema.\n - If this error should be ignored disable `maximum-length-constraint` check in {validator}."
         }
 
 


Test patch:
diff --git a/tests/checks/body/constraints/test_enumerable_constraint.py b/tests/checks/body/constraints/test_enumerable_constraint.py
index 036b4d3..872b9f9 100644
--- a/tests/checks/body/constraints/test_enumerable_constraint.py
+++ b/tests/checks/body/constraints/test_enumerable_constraint.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_enumerable_constraint(log):
+def test_check_enumerable_constraint(log):
     errors = []
     columns = []
     checks.enumerable_constraint(errors, columns, 1)
diff --git a/tests/checks/body/constraints/test_maximum_constraint.py b/tests/checks/body/constraints/test_maximum_constraint.py
index de76495..525e7cf 100644
--- a/tests/checks/body/constraints/test_maximum_constraint.py
+++ b/tests/checks/body/constraints/test_maximum_constraint.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_maximum_constraint(log):
+def test_check_maximum_constraint(log):
     errors = []
     columns = []
     checks.maximum_constraint(errors, columns, 1)
diff --git a/tests/checks/body/constraints/test_maximum_length_constraint.py b/tests/checks/body/constraints/test_maximum_length_constraint.py
index 20958e8..d670378 100644
--- a/tests/checks/body/constraints/test_maximum_length_constraint.py
+++ b/tests/checks/body/constraints/test_maximum_length_constraint.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_maximum_length_constraint(log):
+def test_check_maximum_length_constraint(log):
     errors = []
     columns = []
     checks.maximum_length_constraint(errors, columns, 1)
diff --git a/tests/checks/body/constraints/test_minimum_constraint.py b/tests/checks/body/constraints/test_minimum_constraint.py
index fac226a..5be5783 100644
--- a/tests/checks/body/constraints/test_minimum_constraint.py
+++ b/tests/checks/body/constraints/test_minimum_constraint.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_minimum_constraint(log):
+def test_check_minimum_constraint(log):
     errors = []
     columns = []
     checks.minimum_constraint(errors, columns, 1)
diff --git a/tests/checks/body/constraints/test_minimum_length_constraint.py b/tests/checks/body/constraints/test_minimum_length_constraint.py
index df28305..7e9b0df 100644
--- a/tests/checks/body/constraints/test_minimum_length_constraint.py
+++ b/tests/checks/body/constraints/test_minimum_length_constraint.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_minimum_length_constraint(log):
+def test_check_minimum_length_constraint(log):
     errors = []
     columns = []
     checks.minimum_length_constraint(errors, columns, 1)
diff --git a/tests/checks/body/constraints/test_pattern_constraint.py b/tests/checks/body/constraints/test_pattern_constraint.py
index 9ed9197..ec4f1a0 100644
--- a/tests/checks/body/constraints/test_pattern_constraint.py
+++ b/tests/checks/body/constraints/test_pattern_constraint.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_pattern_constraint(log):
+def test_check_pattern_constraint(log):
     errors = []
     columns = []
     checks.pattern_constraint(errors, columns, 1)
diff --git a/tests/checks/body/constraints/test_required_constraint.py b/tests/checks/body/constraints/test_required_constraint.py
index 40224b2..60508fc 100644
--- a/tests/checks/body/constraints/test_required_constraint.py
+++ b/tests/checks/body/constraints/test_required_constraint.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_required_constraint(log):
+def test_check_required_constraint(log):
     errors = []
     columns = []
     checks.required_constraint(errors, columns, 1)
diff --git a/tests/checks/body/constraints/test_unique_constraint.py b/tests/checks/body/constraints/test_unique_constraint.py
index d89c52a..7c52996 100644
--- a/tests/checks/body/constraints/test_unique_constraint.py
+++ b/tests/checks/body/constraints/test_unique_constraint.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_unique_constraint(log):
+def test_check_unique_constraint(log):
     state = {}
     errors = []
     columns = []
diff --git a/tests/checks/body/test_blank_row.py b/tests/checks/body/test_blank_row.py
index 1df28c4..9c7da79 100644
--- a/tests/checks/body/test_blank_row.py
+++ b/tests/checks/body/test_blank_row.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_blank_row(log):
+def test_check_blank_row(log):
     errors = []
     columns = [
         {'number': 1,
@@ -22,7 +22,7 @@ def test_blank_row(log):
     assert len(columns) == 1
 
 
-def test_blank_row_problem(log):
+def test_check_blank_row_problem(log):
     errors = []
     columns = [
         {'number': 1,
diff --git a/tests/checks/body/test_duplicate_row.py b/tests/checks/body/test_duplicate_row.py
index a5ff368..c7e164b 100644
--- a/tests/checks/body/test_duplicate_row.py
+++ b/tests/checks/body/test_duplicate_row.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_duplicate_row(log):
+def test_check_duplicate_row(log):
     errors = []
     columns1 = [
         {'number': 1,
@@ -31,7 +31,7 @@ def test_duplicate_row(log):
     assert len(columns2) == 1
 
 
-def test_duplicate_row_problem(log):
+def test_check_duplicate_row_problem(log):
     errors = []
     columns1 = [
         {'number': 1,
diff --git a/tests/checks/body/test_extra_value.py b/tests/checks/body/test_extra_value.py
index 218153e..0ff77bf 100644
--- a/tests/checks/body/test_extra_value.py
+++ b/tests/checks/body/test_extra_value.py
@@ -10,7 +10,7 @@ from goodtables import checks
 
 # Test
 
-def test_extra_value(log):
+def test_check_extra_value(log):
     errors = []
     columns = [
         {'number': 1,
@@ -27,7 +27,7 @@ def test_extra_value(log):
     assert len(columns) == 2
 
 
-def test_extra_value_problem(log):
+def test_check_extra_value_problem(log):
     errors = []
     columns = [
         {'number': 1,
diff --git a/tests/checks/body/test_missing_value.py b/tests/checks/body/test_missing_value.py
index 6236edf..6d9a100 100644
--- a/tests/checks/body/test_missing_value.py
+++ b/tests/checks/body/test_missing_value.py
@@ -10,7 +10,7 @@ from goodtables import checks
 
 # Test
 
-def test_missing_value(log):
+def test_check_missing_value(log):
     errors = []
     columns = [
         {'number': 1,
@@ -27,7 +27,7 @@ def test_missing_value(log):
     assert len(columns) == 2
 
 
-def test_missing_value_problem(log):
+def test_check_missing_value_problem(log):
     errors = []
     columns = [
         {'number': 1,
diff --git a/tests/checks/body/test_non_castable_value.py b/tests/checks/body/test_non_castable_value.py
index 33f486b..1b45207 100644
--- a/tests/checks/body/test_non_castable_value.py
+++ b/tests/checks/body/test_non_castable_value.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_non_castable_value(log):
+def test_check_non_castable_value(log):
     errors = []
     columns = [
         {'number': 1,
@@ -23,7 +23,7 @@ def test_non_castable_value(log):
     assert columns[0]['value'] == 1
 
 
-def test_non_castable_value_problem(log):
+def test_check_non_castable_value_problem(log):
     errors = []
     columns = [
         {'number': 1,
diff --git a/tests/checks/head/test_blank_header.py b/tests/checks/head/test_blank_header.py
index 09600a1..167448f 100644
--- a/tests/checks/head/test_blank_header.py
+++ b/tests/checks/head/test_blank_header.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_blank_header(log):
+def test_check_blank_header(log):
     errors = []
     columns = [
         {'number': 1,
@@ -21,7 +21,7 @@ def test_blank_header(log):
     assert len(columns) == 1
 
 
-def test_blank_header_problem(log):
+def test_check_blank_header_problem(log):
     errors = []
     columns = [
         {'number': 1,
diff --git a/tests/checks/head/test_duplicate_header.py b/tests/checks/head/test_duplicate_header.py
index 9f005d7..a5f8fb2 100644
--- a/tests/checks/head/test_duplicate_header.py
+++ b/tests/checks/head/test_duplicate_header.py
@@ -9,7 +9,7 @@ from goodtables import checks
 
 # Test
 
-def test_duplicate_header(log):
+def test_check_duplicate_header(log):
     errors = []
     columns = [
         {'number': 1,
@@ -24,7 +24,7 @@ def test_duplicate_header(log):
     assert len(columns) == 2
 
 
-def test_duplicate_header_problem(log):
+def test_check_duplicate_header_problem(log):
     errors = []
     columns = [
         {'number': 1,
diff --git a/tests/checks/head/test_extra_header.py b/tests/checks/head/test_extra_header.py
index c58b31d..f8efb71 100644
--- a/tests/checks/head/test_extra_header.py
+++ b/tests/checks/head/test_extra_header.py
@@ -10,7 +10,7 @@ from goodtables import checks
 
 # Test
 
-def test_extra_header(log):
+def test_check_extra_header(log):
     errors = []
     columns = [
         {'number': 1,
@@ -26,7 +26,7 @@ def test_extra_header(log):
     assert len(columns) == 2
 
 
-def test_extra_header_infer(log):
+def test_check_extra_header_infer(log):
     errors = []
     columns = [
         {'number': 1,
@@ -42,7 +42,7 @@ def test_extra_header_infer(log):
     assert columns[1]['field'].name == 'name2'
 
 
-def test_extra_header_problem(log):
+def test_check_extra_header_problem(log):
     errors = []
     columns = [
         {'number': 1,
diff --git a/tests/checks/head/test_missing_header.py b/tests/checks/head/test_missing_header.py
index 6b18fbd..d5ad6d0 100644
--- a/tests/checks/head/test_missing_header.py
+++ b/tests/checks/head/test_missing_header.py
@@ -10,7 +10,7 @@ from goodtables import checks
 
 # Test
 
-def test_missing_header(log):
+def test_check_missing_header(log):
     errors = []
     columns = [
         {'number': 1,
@@ -25,7 +25,7 @@ def test_missing_header(log):
     assert len(columns) == 2
 
 
-def test_missing_header_problem(log):
+def test_check_missing_header_problem(log):
     errors = []
     columns = [
         {'number': 1,
diff --git a/tests/checks/head/test_non_matching_header.py b/tests/checks/head/test_non_matching_header.py
index 7ab5b55..675cd8d 100644
--- a/tests/checks/head/test_non_matching_header.py
+++ b/tests/checks/head/test_non_matching_header.py
@@ -10,7 +10,7 @@ from goodtables import checks
 
 # Test
 
-def test_non_matching_header(log):
+def test_check_non_matching_header(log):
     errors = []
     columns = [
         {'number': 1,
@@ -27,7 +27,7 @@ def test_non_matching_header(log):
     assert len(columns) == 3
 
 
-def test_non_matching_header_problem(log):
+def test_check_non_matching_header_problem(log):
     errors = []
     columns = [
         {'number': 1,
@@ -47,7 +47,7 @@ def test_non_matching_header_problem(log):
     assert len(columns) == 1
 
 
-def test_non_matching_header_order_fields(log):
+def test_check_non_matching_header_order_fields(log):
     errors = []
     columns = [
         {'number': 1,
@@ -64,7 +64,7 @@ def test_non_matching_header_order_fields(log):
     assert len(columns) == 3
 
 
-def test_non_matching_header_order_fields_problem(log):
+def test_check_non_matching_header_order_fields_problem(log):
     errors = []
     columns = [
         {'number': 1,
diff --git a/tests/conftest.py b/tests/conftest.py
index 589f74b..1c5f720 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -27,8 +27,6 @@ def log():
             for error in struct:
                 result.append(pack_error(error))
         if isinstance(struct, dict):
-            for error in struct['errors']:
-                result.append(pack_error(error))
             for table_number, table in enumerate(struct['tables'], start=1):
                 for error in table['errors']:
                     result.append(pack_error(error, table_number))
diff --git a/tests/presets/test_datapackage.py b/tests/presets/test_datapackage.py
index 08cc0fc..3a0a398 100644
--- a/tests/presets/test_datapackage.py
+++ b/tests/presets/test_datapackage.py
@@ -9,7 +9,26 @@ from goodtables import presets
 
 # Test
 
-def test_datapackage():
-    errors, tables = presets.datapackage('data/datapackages/valid/datapackage.json')
-    assert len(errors) == 0
+def test_preset_datapackage():
+    warnings, tables = presets.datapackage('data/datapackages/valid/datapackage.json')
+    assert len(warnings) == 0
     assert len(tables) == 2
+
+
+def test_preset_datapackage_non_tabular_datapackage_issue_170():
+    warnings, tables = presets.datapackage('data/non_tabular_datapackage.json')
+    assert len(warnings) == 0
+    assert len(tables) == 0
+
+
+def test_preset_datapackage_mixed_datapackage_issue_170():
+    warnings, tables = presets.datapackage('data/mixed_datapackage.json')
+    assert len(warnings) == 0
+    assert len(tables) == 1
+
+
+def test_preset_datapackage_invalid_json_issue_192():
+    warnings, tables = presets.datapackage('data/invalid_json.json')
+    assert len(warnings) == 1
+    assert len(tables) == 0
+    assert 'Unable to parse JSON' in warnings[0]
diff --git a/tests/presets/test_nested.py b/tests/presets/test_nested.py
index 6f1b646..f17ae93 100644
--- a/tests/presets/test_nested.py
+++ b/tests/presets/test_nested.py
@@ -9,8 +9,8 @@ from goodtables import presets
 
 # Test
 
-def test_nested():
-    errors, tables = presets.nested([
+def test_preset_nested():
+    warnings, tables = presets.nested([
         {'source': 'data/valid.csv'},
         {'source': 'data/invalid.csv', 'preset': 'table'},
         {'source': 'data/datapackages/valid/datapackage.json', 'preset': 'datapackage'},
@@ -20,5 +20,5 @@ def test_nested():
         'nested': presets.nested,
         'datapackage': presets.datapackage,
     })
-    assert len(errors) == 0
+    assert len(warnings) == 0
     assert len(tables) == 6
diff --git a/tests/presets/test_table.py b/tests/presets/test_table.py
index 4d5a5d2..1281a86 100644
--- a/tests/presets/test_table.py
+++ b/tests/presets/test_table.py
@@ -9,7 +9,14 @@ from goodtables import presets
 
 # Test
 
-def test_table():
-    errors, tables = presets.table('data/valid.csv')
-    assert len(errors) == 0
+def test_preset_table():
+    warnings, tables = presets.table('data/valid.csv')
+    assert len(warnings) == 0
     assert len(tables) == 1
+
+
+def test_preset_table_but_got_datapackage_issue_187():
+    warnings, tables = presets.table('data/datapackages/valid/datapackage.json')
+    assert len(warnings) == 1
+    assert len(tables) == 0
+    assert 'Use "datapackage" preset' in warnings[0]
diff --git a/tests/test_inspector.py b/tests/test_inspector.py
index 296f323..82d4626 100644
--- a/tests/test_inspector.py
+++ b/tests/test_inspector.py
@@ -7,7 +7,7 @@ from __future__ import unicode_literals
 from goodtables import Inspector
 
 
-# Tests [table]
+# Preset: table
 
 def test_inspector_table_valid(log):
     inspector = Inspector()
@@ -25,9 +25,9 @@ def test_inspector_table_invalid(log):
         (1, 2, 4, 'missing-value'),
         (1, 3, None, 'duplicate-row'),
         (1, 4, None, 'blank-row'),
-        (1, 5, 5, 'extra-value'),
         (1, 5, 3, 'non-castable-value'),
         (1, 5, 4, 'non-castable-value'),
+        (1, 5, 5, 'extra-value'),
     ]
 
 
@@ -51,7 +51,7 @@ def test_inspector_table_invalid_row_limit(log):
     ]
 
 
-# Tests [datapackage]
+# Preset: datapackage
 
 def test_inspector_datapackage_valid(log):
     inspector = Inspector()
@@ -79,7 +79,7 @@ def test_inspector_datapackage_invalid_table_limit(log):
     ]
 
 
-# Tests [nested]
+# Preset: nested
 
 def test_inspector_tables_invalid(log):
     inspector = Inspector(infer_schema=True)
@@ -95,13 +95,13 @@ def test_inspector_tables_invalid(log):
         (2, 2, 4, 'missing-value'),
         (2, 3, None, 'duplicate-row'),
         (2, 4, None, 'blank-row'),
-        (2, 5, 5, 'extra-value'),
         (2, 5, 3, 'non-castable-value'),
         (2, 5, 4, 'non-castable-value'),
+        (2, 5, 5, 'extra-value'),
     ]
 
 
-# Tests [exceptions]
+# Catch exceptions
 
 def test_inspector_catch_all_open_exceptions(log):
     inspector = Inspector()
@@ -118,3 +118,64 @@ def test_inspector_catch_all_iter_exceptions(log):
     assert log(report) == [
         (1, None, None, 'source-error'),
     ]
+
+
+# Warnings
+
+def test_inspector_warnings_no():
+    inspector = Inspector()
+    source = 'data/datapackages/invalid/datapackage.json'
+    report = inspector.inspect(source, preset='datapackage')
+    assert len(report['warnings']) == 0
+
+
+def test_inspector_warnings_bad_datapackage_json():
+    inspector = Inspector()
+    source = 'data/invalid_json.json'
+    report = inspector.inspect(source, preset='datapackage')
+    assert len(report['warnings']) == 1
+    assert 'Unable to parse JSON' in report['warnings'][0]
+
+
+def test_inspector_warnings_table_limit():
+    inspector = Inspector(table_limit=1)
+    source = 'data/datapackages/invalid/datapackage.json'
+    report = inspector.inspect(source, preset='datapackage')
+    assert len(report['warnings']) == 1
+    assert 'table(s) limit' in report['warnings'][0]
+
+
+def test_inspector_warnings_row_limit():
+    inspector = Inspector(row_limit=1)
+    source = 'data/datapackages/invalid/datapackage.json'
+    report = inspector.inspect(source, preset='datapackage')
+    assert len(report['warnings']) == 2
+    assert 'row(s) limit' in report['warnings'][0]
+    assert 'row(s) limit' in report['warnings'][1]
+
+
+def test_inspector_warnings_error_limit():
+    inspector = Inspector(error_limit=1)
+    source = 'data/datapackages/invalid/datapackage.json'
+    report = inspector.inspect(source, preset='datapackage')
+    assert len(report['warnings']) == 2
+    assert 'error(s) limit' in report['warnings'][0]
+    assert 'error(s) limit' in report['warnings'][1]
+
+
+def test_inspector_warnings_table_and_row_limit():
+    inspector = Inspector(table_limit=1, row_limit=1)
+    source = 'data/datapackages/invalid/datapackage.json'
+    report = inspector.inspect(source, preset='datapackage')
+    assert len(report['warnings']) == 2
+    assert 'table(s) limit' in report['warnings'][0]
+    assert 'row(s) limit' in report['warnings'][1]
+
+
+def test_inspector_warnings_table_and_error_limit():
+    inspector = Inspector(table_limit=1, error_limit=1)
+    source = 'data/datapackages/invalid/datapackage.json'
+    report = inspector.inspect(source, preset='datapackage')
+    assert len(report['warnings']) == 2
+    assert 'table(s) limit' in report['warnings'][0]
+    assert 'error(s) limit' in report['warnings'][1]
diff --git a/tests/test_spec.py b/tests/test_spec.py
index 4eeaf4f..53076ad 100644
--- a/tests/test_spec.py
+++ b/tests/test_spec.py
@@ -11,7 +11,6 @@ import requests
 from goodtables.spec import spec
 
 
-@pytest.mark.xfail
 def test_spec_is_up_to_date():
     origin_spec = requests.get('https://raw.githubusercontent.com/frictionlessdata/data-quality-spec/master/spec.json').json()
     assert spec == origin_spec, 'run `make spec` to update the spec'