Repository: warpnet/salt-lint

Issue:
Feature Request: Add rule for cron state validation
**Is your feature request related to a problem? Please describe.**

[`cron.{present,absent}`](https://docs.saltstack.com/en/master/ref/states/all/salt.states.cron.html#salt.states.cron.present) changes include some surprising gotchas, this is because the identifier defaults to the `- name:`
```bash
identifier
Custom-defined identifier for tracking the cron line for future crontab edits. This defaults to the state name
```

Require `- identifier:` to be present for `cron.{present,absent}` states which would avoid coupling action changing from identifier changing.


**Describe the solution you'd like**
Since name defines both identifier and action by default, I'd recommend that the linter would verify that `- identifier:` was present. It would also verify that the `- identifier:` does not use any jinja templating as it's ideal that the data is not changed external the declaration.

This would make changing the actions not create duplicate crons.

**Describe alternatives you've considered**
My personal opinion is that salt shouldn't be combining the identifier for uniqueness and the command you are running, but I feel that would be a fairly large and breaking api change. 



Discussion:

Patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index fc39d25..abf935e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -20,7 +20,7 @@ jobs:
 
   docker:
     runs-on: ubuntu-latest
-    steps: 
+    steps:
     - name: Checkout
       uses: actions/checkout@v2
     - name: dockerlint
@@ -57,7 +57,7 @@ jobs:
     - name: Lint with codespell
       run: |
         pip install codespell
-        codespell --skip="./.git*"
+        codespell --skip="./.git*,./saltlint/rules/FileManagedReplaceContentRule.py"
 
   pylint:
     runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 5c2f86e..81878e2 100644
--- a/README.md
+++ b/README.md
@@ -184,6 +184,7 @@ Rule | Description
 [212](https://github.com/warpnet/salt-lint/wiki/212) | Most files should not contain irregular spaces
 [213](https://github.com/warpnet/salt-lint/wiki/213) | SaltStack recommends using `cmd.run` together with `onchanges`, rather than `cmd.wait`
 [214](https://github.com/warpnet/salt-lint/wiki/214) | SLS file with a period in the name (besides the suffix period) can not be referenced
+[215](https://github.com/warpnet/salt-lint/wiki/215) | Using `replace: False` is required when not specifying content
 
 ### Jinja
 
diff --git a/saltlint/linter/rule.py b/saltlint/linter/rule.py
index becf140..e006699 100644
--- a/saltlint/linter/rule.py
+++ b/saltlint/linter/rule.py
@@ -7,6 +7,7 @@ import six
 
 from saltlint.utils import get_rule_skips_from_line, get_file_type
 from saltlint.linter.match import Match
+from saltlint.utils import LANGUAGE_SLS
 
 
 class Rule(object):
@@ -86,3 +87,32 @@ class Rule(object):
             matches.append(Match(line, section, file['path'], self, message))
 
         return matches
+
+
+class DeprecationRule(Rule):
+    id = None
+    state = None
+    deprecated_since = None
+
+    severity = 'HIGH'
+    languages = [LANGUAGE_SLS]
+    tags = ['deprecation']
+
+    @property
+    def shortdesc(self):
+        return "State '{}' is deprecated since SaltStack version '{}'".format(
+            self.state, self.deprecated_since
+        )
+
+    @property
+    def description(self):
+        return self.shortdesc
+
+    @property
+    def regex(self):
+        return re.compile(
+            r"^\s{2}" + self.state.replace(".", r"\.") + "(?=:|$)"
+        )
+
+    def match(self, file, line):
+        return self.regex.search(line)
diff --git a/saltlint/rules/CmdWaitRecommendRule.py b/saltlint/rules/CmdWaitRecommendRule.py
index 2fd7405..9a8ce90 100644
--- a/saltlint/rules/CmdWaitRecommendRule.py
+++ b/saltlint/rules/CmdWaitRecommendRule.py
@@ -2,11 +2,11 @@
 # Copyright (c) 2020 Warpnet B.V.
 
 import re
-from saltlint.linter.rule import Rule
+from saltlint.linter.rule import DeprecationRule
 from saltlint.utils import LANGUAGE_SLS
 
 
-class CmdWaitRecommendRule(Rule):
+class CmdWaitRecommendRule(DeprecationRule):
     id = '213'
     shortdesc = 'SaltStack recommends using cmd.run together with onchanges, rather than cmd.wait'
     description = 'SaltStack recommends using cmd.run together with onchanges, rather than cmd.wait'
diff --git a/saltlint/rules/FileManagedReplaceContentRule.py b/saltlint/rules/FileManagedReplaceContentRule.py
new file mode 100644
index 0000000..5eca8c3
--- /dev/null
+++ b/saltlint/rules/FileManagedReplaceContentRule.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+import re
+from saltlint.linter.rule import Rule
+from saltlint.utils import get_rule_skips_from_text
+from saltlint.utils import LANGUAGE_SLS
+
+
+class FileManagedReplaceContentRule(Rule):
+    id = '215'
+    shortdesc = "Using 'replace: False' is required when not specifying content"
+    description = "Using 'replace: False' is required when not specifying content"
+
+    severity = 'HIGH'
+    languages = [LANGUAGE_SLS]
+    tags = ['formatting']
+    version_added = 'develop'
+
+    # Find the full file.managed state
+    regex = re.compile(r"^\s{2}file\.managed:.*(?:\n\s{4}.+)*", re.MULTILINE)
+    # Regex for finding the content source option
+    regex_options= re.compile(
+        r"^\s{4}-\s(?:source:|contents:|contents_pillar:|contents_grains:|replace:\s[F|f]alse).*$",
+        re.MULTILINE
+    )
+
+    def matchtext(self, file, text):
+        results = []
+
+        # Find all file.managed states in the specified sls file
+        for match in re.finditer(self.regex, text):
+            # Continue if the file.managed state includes a content source
+            # or replace is set to False
+            if re.search(self.regex_options, match.group(0)):
+                continue
+
+            # Get the location of the regex match
+            start = match.start()
+            end = match.end()
+
+            # Get the line number of the first character
+            lines = text[:start].splitlines()
+            line_no = len(lines) + 1
+
+            # Skip result if noqa for this rule ID is found in section
+            section = text[start:end]
+            if self.id in get_rule_skips_from_text(section):
+                continue
+
+            # Append the match to the results
+            results.append((line_no, section.splitlines()[0], self.shortdesc))
+
+        return results
diff --git a/saltlint/rules/JinjaCommentHasSpacesRule.py b/saltlint/rules/JinjaCommentHasSpacesRule.py
index 7c54719..cb93f30 100644
--- a/saltlint/rules/JinjaCommentHasSpacesRule.py
+++ b/saltlint/rules/JinjaCommentHasSpacesRule.py
@@ -10,8 +10,8 @@ from saltlint.utils import LANGUAGE_JINJA, LANGUAGE_SLS
 
 class JinjaCommentHasSpacesRule(Rule):
     id = '209'
-    shortdesc = 'Jinja comment should have spaces before and after: {# comment #}'
-    description = 'Jinja comment should have spaces before and after: ``{# comment #}``'
+    shortdesc = "Jinja comment should have spaces before and after: '{# comment #}'"
+    description = "Jinja comment should have spaces before and after: '{# comment #}'"
     severity = 'LOW'
     languages = [LANGUAGE_SLS, LANGUAGE_JINJA]
     tags = ['formatting', 'jinja']
diff --git a/saltlint/rules/JinjaStatementHasSpacesRule.py b/saltlint/rules/JinjaStatementHasSpacesRule.py
index 59992e9..0ef024e 100644
--- a/saltlint/rules/JinjaStatementHasSpacesRule.py
+++ b/saltlint/rules/JinjaStatementHasSpacesRule.py
@@ -10,8 +10,8 @@ from saltlint.utils import LANGUAGE_JINJA, LANGUAGE_SLS
 
 class JinjaStatementHasSpacesRule(Rule):
     id = '202'
-    shortdesc = 'Jinja statement should have spaces before and after: {% statement %}'
-    description = 'Jinja statement should have spaces before and after: ``{% statement %}``'
+    shortdesc = "Jinja statement should have spaces before and after: '{% statement %}'"
+    description = "Jinja statement should have spaces before and after: '{% statement %}'"
     severity = 'LOW'
     languages = [LANGUAGE_SLS, LANGUAGE_JINJA]
     tags = ['formatting', 'jinja']
diff --git a/saltlint/rules/JinjaVariableHasSpacesRule.py b/saltlint/rules/JinjaVariableHasSpacesRule.py
index 53fd0eb..946222b 100644
--- a/saltlint/rules/JinjaVariableHasSpacesRule.py
+++ b/saltlint/rules/JinjaVariableHasSpacesRule.py
@@ -10,8 +10,8 @@ from saltlint.utils import LANGUAGE_JINJA, LANGUAGE_SLS
 
 class JinjaVariableHasSpacesRule(Rule):
     id = '206'
-    shortdesc = 'Jinja variables should have spaces before and after: {{ var_name }}'
-    description = 'Jinja variables should have spaces before and after: ``{{ var_name }}``'
+    shortdesc = "Jinja variables should have spaces before and after: '{{ var_name }}'"
+    description = "Jinja variables should have spaces before and after: '{{ var_name }}'"
     severity = 'LOW'
     languages = [LANGUAGE_SLS, LANGUAGE_JINJA]
     tags = ['formatting', 'jinja']
diff --git a/saltlint/rules/SLSFileNameRule.py b/saltlint/rules/SLSFileNameRule.py
index f251b50..e4b6db4 100644
--- a/saltlint/rules/SLSFileNameRule.py
+++ b/saltlint/rules/SLSFileNameRule.py
@@ -3,6 +3,7 @@
 
 import os
 from saltlint.linter.rule import Rule
+from saltlint.utils import LANGUAGE_SLS
 
 
 class SLSFileNameRule(Rule):
@@ -12,6 +13,7 @@ class SLSFileNameRule(Rule):
     description = ('SLS file with a period in the name (besides the suffix period) can not be '
                    'referenced')
     severity = 'HIGH'
+    languages = [LANGUAGE_SLS]
     tags = ['formatting']
     version_added = 'develop'
 
diff --git a/saltlint/rules/StateDeprecationDockerAbsent.py b/saltlint/rules/StateDeprecationDockerAbsent.py
new file mode 100644
index 0000000..50955be
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerAbsent.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerAbsent(DeprecationRule):
+    id = '906'
+    state = 'docker.absent'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationDockerImageAbsent.py b/saltlint/rules/StateDeprecationDockerImageAbsent.py
new file mode 100644
index 0000000..74e65fc
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerImageAbsent.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerImageAbsent(DeprecationRule):
+    id = '907'
+    state = 'docker.image_absent'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationDockerImagePresent.py b/saltlint/rules/StateDeprecationDockerImagePresent.py
new file mode 100644
index 0000000..cae5946
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerImagePresent.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerImagePresent(DeprecationRule):
+    id = '908'
+    state = 'docker.image_present'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationDockerModWatch.py b/saltlint/rules/StateDeprecationDockerModWatch.py
new file mode 100644
index 0000000..34f618a
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerModWatch.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerModWatch(DeprecationRule):
+    id = '909'
+    state = 'docker.mod_watch'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationDockerNetworkAbsent.py b/saltlint/rules/StateDeprecationDockerNetworkAbsent.py
new file mode 100644
index 0000000..95a20b9
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerNetworkAbsent.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerNetworkAbsent(DeprecationRule):
+    id = '910'
+    state = 'docker.network_absent'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationDockerNetworkPresent.py b/saltlint/rules/StateDeprecationDockerNetworkPresent.py
new file mode 100644
index 0000000..2d0bc6d
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerNetworkPresent.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerNetworkPresent(DeprecationRule):
+    id = '911'
+    state = 'docker.network_present'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationDockerRunning.py b/saltlint/rules/StateDeprecationDockerRunning.py
new file mode 100644
index 0000000..e88b375
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerRunning.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerRunning(DeprecationRule):
+    id = '912'
+    state = 'docker.running'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationDockerStopped.py b/saltlint/rules/StateDeprecationDockerStopped.py
new file mode 100644
index 0000000..2995919
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerStopped.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerStopped(DeprecationRule):
+    id = '913'
+    state = 'docker.stopped'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationDockerVolumeAbsent.py b/saltlint/rules/StateDeprecationDockerVolumeAbsent.py
new file mode 100644
index 0000000..f506d69
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerVolumeAbsent.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerVolumeAbsent(DeprecationRule):
+    id = '914'
+    state = 'docker.volume_absent'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationDockerVolumePresent.py b/saltlint/rules/StateDeprecationDockerVolumePresent.py
new file mode 100644
index 0000000..36d4563
--- /dev/null
+++ b/saltlint/rules/StateDeprecationDockerVolumePresent.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationDockerVolumePresent(DeprecationRule):
+    id = '915'
+    state = 'docker.volume_present'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationElasticsearchIndexRule.py b/saltlint/rules/StateDeprecationElasticsearchIndexRule.py
new file mode 100644
index 0000000..e12a7c3
--- /dev/null
+++ b/saltlint/rules/StateDeprecationElasticsearchIndexRule.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationElasticsearchIndexRule(DeprecationRule):
+    id = '902'
+    state = 'elasticsearch_index.absent'
+    deprecated_since = '2017.7.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationVirtRevertedRule.py b/saltlint/rules/StateDeprecationVirtRevertedRule.py
new file mode 100644
index 0000000..89bc21a
--- /dev/null
+++ b/saltlint/rules/StateDeprecationVirtRevertedRule.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationVirtRevertedRule(DeprecationRule):
+    id = '903'
+    state = 'virt.reverted'
+    deprecated_since = '2016.3.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationVirtSavedRule.py b/saltlint/rules/StateDeprecationVirtSavedRule.py
new file mode 100644
index 0000000..17cafed
--- /dev/null
+++ b/saltlint/rules/StateDeprecationVirtSavedRule.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationVirtSavedRule(DeprecationRule):
+    id = '904'
+    state = 'virt.saved'
+    deprecated_since = '2016.3.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/StateDeprecationVirtUnpoweredRule.py b/saltlint/rules/StateDeprecationVirtUnpoweredRule.py
new file mode 100644
index 0000000..b153eef
--- /dev/null
+++ b/saltlint/rules/StateDeprecationVirtUnpoweredRule.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+from saltlint.linter.rule import DeprecationRule
+
+
+class StateDeprecationVirtUnpoweredRule(DeprecationRule):
+    id = '905'
+    state = 'virt.unpowered'
+    deprecated_since = '2016.3.0'
+    version_added = 'develop'
diff --git a/saltlint/rules/YamlHasOctalValueRule.py b/saltlint/rules/YamlHasOctalValueRule.py
index 57b4f82..2001e49 100644
--- a/saltlint/rules/YamlHasOctalValueRule.py
+++ b/saltlint/rules/YamlHasOctalValueRule.py
@@ -10,8 +10,8 @@ from saltlint.utils import LANGUAGE_SLS
 
 class YamlHasOctalValueRule(Rule):
     id = '210'
-    shortdesc = 'Numbers that start with `0` should always be encapsulated in quotation marks'
-    description = 'Numbers that start with `0` should always be encapsulated in quotation marks'
+    shortdesc = "Numbers that start with '0' should always be encapsulated in quotation marks"
+    description = "Numbers that start with '0' should always be encapsulated in quotation marks"
     severity = 'HIGH'
     languages = [LANGUAGE_SLS]
     tags = ['formatting']


Test patch:
diff --git a/tests/unit/TestDeprecationRule.py b/tests/unit/TestDeprecationRule.py
new file mode 100644
index 0000000..b52c632
--- /dev/null
+++ b/tests/unit/TestDeprecationRule.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+import unittest
+
+from saltlint.linter.collection import RulesCollection
+from saltlint.rules.StateDeprecationDockerAbsent import StateDeprecationDockerAbsent
+from tests import RunFromText
+
+
+GOOD_STATES_LINE = '''
+example:
+  docker_container.absent:
+    - name: example
+
+example:
+  docker_container.absent
+'''
+
+BAD_STATES_LINE = '''
+example:
+  docker.absent:
+    - name: example
+
+example:
+  docker.absent
+'''
+
+class TestFileModeLeadingZeroRule(unittest.TestCase):
+    collection = RulesCollection()
+
+    def setUp(self):
+        self.collection.register(StateDeprecationDockerAbsent())
+
+    def test_statement_positive(self):
+        runner = RunFromText(self.collection)
+        results = runner.run_state(GOOD_STATES_LINE)
+        self.assertEqual(0, len(results))
+
+    def test_statement_negative(self):
+        runner = RunFromText(self.collection)
+        results = runner.run_state(BAD_STATES_LINE)
+        self.assertEqual(2, len(results))
diff --git a/tests/unit/TestFileManagedReplaceContentRule.py b/tests/unit/TestFileManagedReplaceContentRule.py
new file mode 100644
index 0000000..76d453e
--- /dev/null
+++ b/tests/unit/TestFileManagedReplaceContentRule.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
+
+import unittest
+
+from saltlint.linter.collection import RulesCollection
+from saltlint.rules.FileManagedReplaceContentRule import FileManagedReplaceContentRule
+from tests import RunFromText
+
+
+GOOD_FILE_STATE = '''
+cis_grub.cfg:
+  file.managed:
+    - name: /boot/grub.cfg
+    - user: root
+    - group: root
+    - mode: '0700'
+    - source: salt://grub/files/grub.cfg
+
+cis_systemid_only_set_once:
+  file.managed:
+    - name: /tmp/systemid
+    - user: root
+    - group: root
+    - replace: False
+    - contents_grains: osmajorrelease
+
+user:
+  user.present:
+    - name: "salt-lint"
+  file.managed:
+    - name: /user/salt-lint/.bashrc
+    - user: root
+    - group: root
+    - mode: '0700'
+    - contents_pillar: bashrc
+
+cis_grub.cfg_managerights:
+  file.managed:
+    - name: /boot/grub.cfg
+    - user: root
+    - group: root
+    - mode: '0700'
+    - replace: False
+
+cis_grub_permissions:
+  file.managed:
+    - name: /boot/grub.cfg
+    - replace: false
+    - user: root
+    - group: root
+    - mode: '0700'
+'''
+
+BAD_FILE_STATE = '''
+cis_grub.cfg:
+  file.managed:
+    - name: /boot/grub.cfg
+    - user: root
+    - group: root
+    - mode: '0700'
+
+cis_systemid_only_set_once:
+  file.managed:
+    - name: /tmp/systemid
+    - user: root
+    - group: root
+    - replace: True
+
+user:
+  user.present:
+    - name: "salt-lint"
+  file.managed:
+    - name: /user/salt-lint/.bashrc
+    - user: root
+    - group: root
+    - mode: '0700'
+
+cis_grub_permissions:
+  file.managed: # noqa: 215
+    - name: /boot/grub.cfg
+    - user: root
+    - group: root
+    - mode: '0700'
+'''
+
+
+class TestFileManagedReplaceContentRule(unittest.TestCase):
+    collection = RulesCollection()
+
+    def setUp(self):
+        self.collection.register(FileManagedReplaceContentRule())
+
+    def test_statement_positive(self):
+        runner = RunFromText(self.collection)
+        results = runner.run_state(GOOD_FILE_STATE)
+        self.assertEqual(0, len(results))
+
+    def test_statement_negative(self):
+        runner = RunFromText(self.collection)
+        results = runner.run_state(BAD_FILE_STATE)
+        self.assertEqual(3, len(results))
+
+        # Check line numbers of the results
+        self.assertEqual(3, results[0].linenumber)
+        self.assertEqual(10, results[1].linenumber)
+        self.assertEqual(19, results[2].linenumber)
diff --git a/tests/unit/TestSLSFileNameRule.py b/tests/unit/TestSLSFileNameRule.py
index 770070a..924306e 100644
--- a/tests/unit/TestSLSFileNameRule.py
+++ b/tests/unit/TestSLSFileNameRule.py
@@ -24,8 +24,11 @@ class TestSLSFileNameRule(unittest.TestCase):
         runner = Runner(self.collection, path, Configuration())
         self.assertEqual([], runner.run())
 
+        path = 'tests/test-extension-failure.extra.jinja'
+        runner = Runner(self.collection, path, Configuration())
+        self.assertEqual([], runner.run())
+
     def test_file_negative(self):
         path = 'tests/test-extension-failure.extra.sls'
         runner = Runner(self.collection, path, Configuration())
-        errors = runner.run()
-        self.assertEqual(1, len(errors))
+        self.assertEqual(1, len(runner.run()))