Repository: jamescooke/flake8-aaa

Issue:
Add AAA03
When there exists an Arrange block, then:

"Expected 1 blank line before Act block, found none"
Add AAA04
When there is an Assert block, then

"Expected 1 blank line before Assert block, found none"

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3154922..74dcba5 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,6 +16,8 @@ Added
 
 * New rule ``AAA03`` "expected 1 blank line before Act block, found none"
 
+* New rule ``AAA04`` "expected 1 blank line before Assert block, found none"
+
 0.2.0_ - 2018/05/28
 -------------------
 
diff --git a/flake8_aaa/arrange_block.py b/flake8_aaa/arrange_block.py
index a27fb24..bca488c 100644
--- a/flake8_aaa/arrange_block.py
+++ b/flake8_aaa/arrange_block.py
@@ -1,3 +1,6 @@
+import ast
+
+
 class ArrangeBlock:
     """
     Attributes:
@@ -8,4 +11,16 @@ class ArrangeBlock:
         self.nodes = []
 
     def add_node(self, node):
+        """
+        Add node if it's an "arrangement node".
+
+        Returns:
+            bool: Node is an arrangement node.
+        """
+        if isinstance(node, ast.Pass):
+            return False
+        if isinstance(node, ast.Expr) and isinstance(node.value, ast.Str):
+            return False
+
         self.nodes.append(node)
+        return True
diff --git a/flake8_aaa/assert_block.py b/flake8_aaa/assert_block.py
new file mode 100644
index 0000000..c74a8f4
--- /dev/null
+++ b/flake8_aaa/assert_block.py
@@ -0,0 +1,11 @@
+class AssertBlock:
+    """
+    Attributes:
+        nodes (list (ast Node))
+    """
+
+    def __init__(self):
+        self.nodes = []
+
+    def add_node(self, node):
+        self.nodes.append(node)
diff --git a/flake8_aaa/function.py b/flake8_aaa/function.py
index a10c8aa..44eab60 100644
--- a/flake8_aaa/function.py
+++ b/flake8_aaa/function.py
@@ -1,5 +1,6 @@
 from .act_block import ActBlock
 from .arrange_block import ArrangeBlock
+from .assert_block import AssertBlock
 from .exceptions import NotActionBlock, ValidationError
 from .helpers import function_is_noop
 from .types import ActBlockType
@@ -38,6 +39,8 @@ class Function:
         self.act_block = self.load_act_block()
         self.arrange_block = self.load_arrange_block()
         self.check_act_arrange_spacing()
+        self.assert_block = self.load_assert_block()
+        self.check_act_assert_spacing()
 
     def load_act_block(self):
         """
@@ -71,9 +74,6 @@ class Function:
         """
         Returns:
             ArrangeBlock: Or ``None`` if no Act block is found.
-
-        Raises:
-            ValidationError
         """
         arrange_block = ArrangeBlock()
         for node in self.node.body:
@@ -86,6 +86,21 @@ class Function:
 
         return None
 
+    def load_assert_block(self):
+        """
+        Returns:
+            AssertBlock: Or ``None`` if no Assert block is found.
+        """
+        assert_block = AssertBlock()
+        for node in self.node.body:
+            if node.lineno > self.act_block.node.lineno:
+                assert_block.add_node(node)
+
+        if len(assert_block.nodes) > 0:
+            return assert_block
+
+        return None
+
     def check_act_arrange_spacing(self):
         """
         When Function has an Arrange block, then ensure that there is a blank
@@ -110,6 +125,20 @@ class Function:
                     text='AAA03 expected 1 blank line before Act block, found none',
                 )
 
+    def check_act_assert_spacing(self):
+        """
+        Raises:
+            ValidationError: When no space found
+        """
+        if self.assert_block:
+            line_before_assert = self.get_line_relative_to_node(self.assert_block.nodes[0], -1)
+            if line_before_assert != '\n':
+                raise ValidationError(
+                    line_number=self.assert_block.nodes[0].lineno,
+                    offset=self.assert_block.nodes[0].col_offset,
+                    text='AAA04 expected 1 blank line before Assert block, found none',
+                )
+
     def get_line_relative_to_node(self, target_node, offset):
         """
         Args:


Test patch:
diff --git a/tests/arrange_block/test_add_node.py b/tests/arrange_block/test_add_node.py
index 919c3be..6f77bac 100644
--- a/tests/arrange_block/test_add_node.py
+++ b/tests/arrange_block/test_add_node.py
@@ -1,16 +1,56 @@
 import pytest
 
 from flake8_aaa.arrange_block import ArrangeBlock
+from flake8_aaa.function import Function
 
 
-@pytest.mark.parametrize('code_str', ['''
+@pytest.fixture
+def function(first_node_with_tokens, lines):
+    """
+    Returns:
+        Function: Loaded with ``first_node_with_tokens`` node, lines of test
+        are passed to Function.
+    """
+    return Function(first_node_with_tokens, lines)
+
+
+# --- TESTS ---
+
+
+@pytest.mark.parametrize(
+    'code_str',
+    ['''
+def test():
+    x = 1
+'''],
+    ids=['assignment'],
+)
+def test(function):
+    arrange_block = ArrangeBlock()
+
+    result = arrange_block.add_node(function.node.body[0])
+
+    assert result is True
+    assert arrange_block.nodes == [function.node.body[0]]
+
+
+@pytest.mark.parametrize(
+    'code_str',
+    ['''
+def test():
+    """
+    Do nothing
+    """
+''', '''
 def test():
     pass
-'''])
-def test(first_node_with_tokens):
+'''],
+    ids=['docstring', 'pass'],
+)
+def test_not_loaded(function):
     arrange_block = ArrangeBlock()
 
-    result = arrange_block.add_node(first_node_with_tokens)
+    result = arrange_block.add_node(function.node.body[0])
 
-    assert result is None
-    assert arrange_block.nodes == [first_node_with_tokens]
+    assert result is False
+    assert len(arrange_block.nodes) == 0
diff --git a/tests/function/conftest.py b/tests/function/conftest.py
index 693eb5a..7a2acd5 100644
--- a/tests/function/conftest.py
+++ b/tests/function/conftest.py
@@ -31,3 +31,13 @@ def function_with_arrange_act_blocks(function_with_act_block):
     """
     function_with_act_block.arrange_block = function_with_act_block.load_arrange_block()
     return function_with_act_block
+
+
+@pytest.fixture
+def function_aaa_blocks(function_with_arrange_act_blocks):
+    """
+    Returns:
+        Function: With Arrange, Act, Assert blocks loaded.
+    """
+    function_with_arrange_act_blocks.assert_block = function_with_arrange_act_blocks.load_assert_block()
+    return function_with_arrange_act_blocks
diff --git a/tests/function/test_check_act_arrange_spacing.py b/tests/function/test_check_act_arrange_spacing.py
index c06e9c9..0c4b0a5 100644
--- a/tests/function/test_check_act_arrange_spacing.py
+++ b/tests/function/test_check_act_arrange_spacing.py
@@ -21,7 +21,8 @@ def test_no_arrange(function_with_arrange_act_blocks):
 
 @pytest.mark.parametrize(
     'code_str',
-    ['''
+    [
+        '''
 def test():
     x = 1
     y = 2
@@ -29,8 +30,18 @@ def test():
     result = x + y
 
     assert result == 3
-'''],
-    ids=['well spaced test'],
+''', '''
+def test(person):
+    """
+    Person can't be loaded
+    """
+    with pytest.raises(KeyError):
+        person.load(-1)
+
+    assert person.loaded is False
+'''
+    ],
+    ids=['well spaced test', 'spaced test with pytest.raises'],
 )
 def test_has_act_block_good_spacing(function_with_arrange_act_blocks):
     result = function_with_arrange_act_blocks.check_act_arrange_spacing()
diff --git a/tests/function/test_check_act_assert_spacing.py b/tests/function/test_check_act_assert_spacing.py
new file mode 100644
index 0000000..3c63bfc
--- /dev/null
+++ b/tests/function/test_check_act_assert_spacing.py
@@ -0,0 +1,39 @@
+import pytest
+
+from flake8_aaa.exceptions import ValidationError
+
+
+@pytest.mark.parametrize(
+    'code_str', ['''
+def test():
+    with pytest.raises(KeyError):
+        load_news('tomorrow')
+'''],
+    ids=['no assert']
+)
+def test_no_assert(function_aaa_blocks):
+    result = function_aaa_blocks.check_act_assert_spacing()
+
+    assert result is None
+
+
+@pytest.mark.parametrize(
+    'code_str', ['''
+
+
+
+def test():
+    result = 1 + 2
+    assert result == 3
+'''], ids=['no spacing']
+)
+def test_no_spacing(function_aaa_blocks):
+    """
+    Exception points at first line of assert block
+    """
+    with pytest.raises(ValidationError) as excinfo:
+        function_aaa_blocks.check_act_assert_spacing()
+
+    assert excinfo.value.line_number == 7
+    assert excinfo.value.offset == 4
+    assert excinfo.value.text.startswith('AAA04 ')
diff --git a/tests/function/test_check_all.py b/tests/function/test_check_all.py
index eccdfba..7eb782c 100644
--- a/tests/function/test_check_all.py
+++ b/tests/function/test_check_all.py
@@ -44,3 +44,32 @@ def test_missing_space_before_act(function):
     assert excinfo.value.line_number == 4
     assert excinfo.value.offset == 4
     assert excinfo.value.text == 'AAA03 expected 1 blank line before Act block, found none'
+
+
+@pytest.mark.parametrize(
+    'code_str',
+    [
+        '''
+def test(file_resource):
+    file_resource.connect()
+
+    result = file_resource.retrieve()
+    assert result.success is True
+''',
+        '''
+def test_push(queue):
+    item = Document()
+
+    queue.push(item)  # act
+    assert queue.pop() == item
+''',
+    ],
+    ids=['no line before assert', 'no line before assert with marked act'],
+)
+def test_missing_space_before_assert(function):
+    with pytest.raises(ValidationError) as excinfo:
+        function.check_all()
+
+    assert excinfo.value.line_number == 6
+    assert excinfo.value.offset == 4
+    assert excinfo.value.text == 'AAA04 expected 1 blank line before Assert block, found none'
diff --git a/tests/function/test_load_arrange_block.py b/tests/function/test_load_arrange_block.py
index c085a35..5f430e9 100644
--- a/tests/function/test_load_arrange_block.py
+++ b/tests/function/test_load_arrange_block.py
@@ -10,8 +10,6 @@ def test():
     assert result == 1
 '''])
 def test_none(function_with_act_block):
-    function_with_act_block.load_act_block()
-
     result = function_with_act_block.load_arrange_block()
 
     assert result is None
@@ -29,8 +27,6 @@ def test():
 ''']
 )
 def test_simple(function_with_act_block):
-    function_with_act_block.load_act_block()
-
     result = function_with_act_block.load_arrange_block()
 
     assert isinstance(result, ArrangeBlock)
diff --git a/tests/function/test_load_assert_block.py b/tests/function/test_load_assert_block.py
new file mode 100644
index 0000000..e3d1f59
--- /dev/null
+++ b/tests/function/test_load_assert_block.py
@@ -0,0 +1,42 @@
+import pytest
+
+from flake8_aaa.assert_block import AssertBlock
+
+
+@pytest.mark.parametrize(
+    'code_str', ['''
+def test():
+    with pytest.raises(DoesNotExist):
+        load_account(-1)
+''']
+)
+def test_none(function_with_act_block):
+    result = function_with_act_block.load_assert_block()
+
+    assert result is None
+
+
+@pytest.mark.parametrize(
+    'code_str', [
+        '''
+def test():
+    result = load_account(1)
+
+    assert isinstance(result, Account)
+    assert result.id == 1
+''',
+        '''
+def test_raise(toy_instance):
+    with pytest.raises(PlayError) as excinfo:
+        toy_instance.run()
+
+    assert 'Not ready' in excinfo.value.message
+    assert toy_instance.has_run is False
+''',
+    ]
+)
+def test(function_with_act_block):
+    result = function_with_act_block.load_assert_block()
+
+    assert isinstance(result, AssertBlock)
+    assert len(result.nodes) == 2
diff --git a/tests/test_run.py b/tests/test_run.py
index 8263329..b8767f9 100644
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -59,6 +59,7 @@ def test_act(flake8dir):
         test_plus='''
             def test():
                 x = 1 + 1  # act
+
                 assert x == 2
         ''',
     )