#!/usr/bin/python
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Test the lint module."""

import collections
import os
import sys

sys.path.insert(0, os.path.abspath('%s/../../..' % os.path.dirname(__file__)))

from chromite.lib import cros_test_lib
import lint


class TestNode(object):
  """Object good enough to stand in for lint funcs"""

  Args = collections.namedtuple('Args', ('args', 'vararg', 'kwarg'))
  Arg = collections.namedtuple('Arg', ('name',))

  def __init__(self, doc='', fromlineno=0, path='foo.py', args=(), vararg='',
               kwarg=''):
    self.doc = doc
    self.lines = doc.split('\n')
    self.fromlineno = fromlineno
    self.file = path
    self.args = self.Args(args=[self.Arg(name=x) for x in args],
                          vararg=vararg, kwarg=kwarg)

  def argnames(self):
    return self.args


class DocStringCheckerTest(cros_test_lib.TestCase):
  """Tests for DocStringChecker module"""

  GOOD_FUNC_DOCSTRINGS = (
      'Some string',
      """Short summary

      Body of text.
      """,
      """line o text

      Body and comments on
      more than one line.

      Args:
        moo: cow

      Returns:
        some value

      Raises:
        something else
      """,
      """Short summary.

      Args:
        fat: cat

      Yields:
        a spoon
      """,
  )

  BAD_FUNC_DOCSTRINGS = (
      """
      bad first line
      """,
      """ whitespace is wrong""",
      """whitespace is wrong	""",
      """Should be no trailing blank lines

      Returns:
        a value

      """
      """ok line

      cuddled end""",
      """we want Args/Returns not Arguments/Return

      Arguments:
      Return:
      """,
      """section order is wrong here

      Raises:
      Returns:
      """,
      """sections lack whitespace between them

      Args:
        foo: bar
      Returns:
        yeah
      """,
      """yields is misspelled

      Yield:
        a car
      """,
      """Section name has bad spacing

      Args:\x20\x20\x20
        key: here
      """,
      """too many blank lines


      Returns:
        None
      """,
      """wrongly uses javadoc

      @returns None
      """
  )

  # The current linter isn't good enough yet to detect these.
  TODO_BAD_FUNC_DOCSTRINGS = (
      """The returns section isn't a proper section

      Args:
        bloop: de

      returns something
      """,
      """the indentation is incorrect

        Args:
          some: day
      """,
  )

  def add_message(self, msg_id, node=None, line=None, args=None):
    """Capture lint checks"""
    # We include node.doc here explicitly so the pretty assert message
    # inclues it in the output automatically.
    self.results.append((msg_id, node.doc, line, args))

  def setUp(self):
    self.results = []
    self.checker = lint.DocStringChecker()
    self.checker.add_message = self.add_message

  def testGood_visit_function(self):
    """Allow known good docstrings"""
    for dc in self.GOOD_FUNC_DOCSTRINGS:
      self.results = []
      node = TestNode(doc=dc)
      self.checker.visit_function(node)
      self.assertEqual(self.results, [],
                       msg='docstring was not accepted:\n"""%s"""' % dc)

  def testBad_visit_function(self):
    """Reject known bad docstrings"""
    for dc in self.BAD_FUNC_DOCSTRINGS:
      self.results = []
      node = TestNode(doc=dc)
      self.checker.visit_function(node)
      self.assertNotEqual(self.results, [],
                          msg='docstring was not rejected:\n"""%s"""' % dc)

  def testSmoke_visit_module(self):
    """Smoke test for modules"""
    self.checker.visit_module(TestNode(doc='foo'))
    self.assertEqual(self.results, [])
    self.checker.visit_module(TestNode(doc='', path='/foo/__init__.py'))
    self.assertEqual(self.results, [])

  def testSmoke_visit_class(self):
    """Smoke test for classes"""
    self.checker.visit_class(TestNode(doc='bar'))

  def testGood_check_first_line(self):
    """Verify _check_first_line accepts good inputs"""
    # pylint: disable=W0212
    docstrings = (
        'Some string',
    )
    for dc in docstrings:
      self.results = []
      node = TestNode(doc=dc)
      self.checker._check_first_line(node, node.lines)
      self.assertEqual(self.results, [],
                       msg='docstring was not accepted:\n"""%s"""' % dc)

  def testBad_check_first_line(self):
    """Verify _check_first_line rejects bad inputs"""
    # pylint: disable=W0212
    docstrings = (
        '\nSome string\n',
    )
    for dc in docstrings:
      self.results = []
      node = TestNode(doc=dc)
      self.checker._check_first_line(node, node.lines)
      self.assertEqual(len(self.results), 1)

  def testGoodFuncVarKwArg(self):
    """Check valid inputs for *args and **kwargs"""
    # pylint: disable=W0212
    for vararg in (None, 'args', '_args'):
      for kwarg in (None, 'kwargs', '_kwargs'):
        self.results = []
        node = TestNode(vararg=vararg, kwarg=kwarg)
        self.checker._check_func_signature(node)
        self.assertEqual(len(self.results), 0)

  def testMisnamedFuncVarKwArg(self):
    """Reject anything but *args and **kwargs"""
    # pylint: disable=W0212
    for vararg in ('arg', 'params', 'kwargs', '_moo'):
      self.results = []
      node = TestNode(vararg=vararg)
      self.checker._check_func_signature(node)
      self.assertEqual(len(self.results), 1)

    for kwarg in ('kwds', '_kwds', 'args', '_moo'):
      self.results = []
      node = TestNode(kwarg=kwarg)
      self.checker._check_func_signature(node)
      self.assertEqual(len(self.results), 1)

  def testGoodFuncArgs(self):
    """Verify normal args in Args are allowed"""
    # pylint: disable=W0212
    datasets = (
        ("""args are correct, and cls is ignored

         Args:
           moo: cow
         """,
         ('cls', 'moo',), None, None,
        ),
        ("""args are correct, and self is ignored

         Args:
           moo: cow
           *args: here
         """,
         ('self', 'moo',), 'args', 'kwargs',
        ),
        ("""args are allowed to wrap

         Args:
           moo:
             a big fat cow
             that takes many lines
             to describe its fatness
         """,
         ('moo',), None, 'kwargs',
        ),
    )
    for dc, args, vararg, kwarg in datasets:
      self.results = []
      node = TestNode(doc=dc, args=args, vararg=vararg, kwarg=kwarg)
      self.checker._check_all_args_in_doc(node, node.lines)
      self.assertEqual(len(self.results), 0)

  def testBadFuncArgs(self):
    """Verify bad/missing args in Args are caught"""
    # pylint: disable=W0212
    datasets = (
        ("""missing 'bar'

         Args:
           moo: cow
         """,
         ('moo', 'bar',),
        ),
        ("""missing 'cow' but has 'bloop'

         Args:
           moo: cow
         """,
         ('bloop',),
        ),
        ("""too much space after colon

         Args:
           moo:  cow
         """,
         ('moo',),
        ),
        ("""not enough space after colon

         Args:
           moo:cow
         """,
         ('moo',),
        ),
    )
    for dc, args in datasets:
      self.results = []
      node = TestNode(doc=dc, args=args)
      self.checker._check_all_args_in_doc(node, node.lines)
      self.assertEqual(len(self.results), 1)


if __name__ == '__main__':
  cros_test_lib.main()
