├── rflint ├── rules │ ├── __init__.py │ ├── resourceRules.py │ ├── keywordRules.py │ ├── otherRules.py │ ├── duplicates.py │ ├── testcaseRules.py │ └── suiteRules.py ├── version.py ├── __init__.py ├── exceptions.py ├── parser │ ├── __init__.py │ ├── rfkeyword.py │ ├── util.py │ ├── testcase.py │ ├── common.py │ ├── tables.py │ └── parser.py ├── __main__.py ├── common.py └── rflint.py ├── requirements-dev.txt ├── MANIFEST.in ├── testdata └── __init__.robot ├── tests ├── unit │ ├── __init__.robot │ ├── UnitTestResources.robot │ ├── keyword.robot │ ├── testcase.robot │ └── robotfile.robot ├── conf │ ├── smoke.args │ └── default.args └── acceptance │ ├── issues │ ├── issue-30.robot │ ├── issue-31.robot │ ├── issue-24.robot │ └── issue-41.robot │ ├── rules │ ├── DuplicateTestNames.robot │ ├── DuplicateKeywordNames.robot │ ├── DuplicateSettings.robot │ ├── TrailingWhitespace.robot │ ├── PeriodInSuiteName.robot │ ├── PeriodInTestName.robot │ ├── LineTooLong.robot │ ├── TooFewTestSteps.robot │ ├── TooFewKeywordSteps.robot │ ├── DuplicateVariables.robot │ ├── FileTooLong.robot │ ├── TooManyTestCases.robot │ ├── TooManyTestSteps.robot │ ├── InvalidTable.robot │ └── InvalidTableInResource.robot │ ├── unicode.robot │ ├── SharedKeywords.py │ ├── self-test.robot │ ├── arguments.robot │ ├── SharedKeywords.robot │ └── smoke.robot ├── test_data ├── keywords.robot ├── acceptance │ ├── 名稱與unicode.robot │ ├── issue-35.robot │ ├── issue-37.robot │ ├── rules │ │ ├── DuplicateVariables_Data.robot │ │ ├── DuplicateVariablesInResource_Data.robot │ │ ├── PeriodInTestName.robot │ │ ├── DuplicateKeywordNames_Data.robot │ │ ├── DuplicateTestNames_Data.robot │ │ ├── DuplicateSettings_Data.robot │ │ ├── TooFewKeywordSteps_Data.robot │ │ ├── TooFewTestSteps_Data.robot │ │ ├── PeriodInSuiteName.foo.robot │ │ ├── InvalidTableInResource_Data.robot │ │ ├── TooManyTestSteps.robot │ │ ├── LineTooLong_Data.robot │ │ ├── InvalidTable_Data.robot │ │ ├── TooManyTestCases.robot │ │ └── FileTooLong_Data.robot │ ├── nodoc.robot │ ├── issue-31.py │ ├── issue-24.robot │ ├── issue-30.py │ ├── issue-41.robot │ └── customRules.py └── pipes.robot ├── setup.cfg ├── .travis.yml ├── TESTING.md ├── .gitignore ├── Makefile ├── setup.py ├── README.md ├── CHANGELOG.md └── LICENSE /rflint/rules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | -------------------------------------------------------------------------------- /rflint/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.1" 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | 4 | -------------------------------------------------------------------------------- /testdata/__init__.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Force tags | test-data 3 | -------------------------------------------------------------------------------- /tests/unit/__init__.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Force tags | unit-test -------------------------------------------------------------------------------- /test_data/keywords.robot: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | | Example Keyword 3 | | | log | hello, world 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | license_files = LICENSE 4 | universal=1 -------------------------------------------------------------------------------- /tests/conf/smoke.args: -------------------------------------------------------------------------------- 1 | --argumentfile tests/conf/default.args 2 | --include smoke 3 | --loglevel DEBUG 4 | ./tests 5 | -------------------------------------------------------------------------------- /test_data/acceptance/名稱與unicode.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Example test case with no documentation 3 | log hello world 4 | 5 | -------------------------------------------------------------------------------- /test_data/acceptance/issue-35.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Пример тест-кейса. 3 | [Tags] example 4 | Log Test-case with non-ASCII symbols in its name. 5 | -------------------------------------------------------------------------------- /test_data/acceptance/issue-37.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | # The following lines have trailing whitespace. Don't delete them! 3 | This line has trailing whitespace 4 | log so does this line. 5 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/DuplicateVariables_Data.robot: -------------------------------------------------------------------------------- 1 | *** Variables *** 2 | ${some_var}= foo 3 | ${some_var}= bar 4 | ${SomeVar}= baz 5 | 6 | *** Test Cases *** 7 | Test1 No Operation 8 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/DuplicateVariablesInResource_Data.robot: -------------------------------------------------------------------------------- 1 | *** Variables *** 2 | ${some_var}= foo 3 | ${some_var}= bar 4 | ${SomeVar}= baz 5 | 6 | *** Keyword *** 7 | Keyword 1 No Operation 8 | -------------------------------------------------------------------------------- /rflint/__init__.py: -------------------------------------------------------------------------------- 1 | from .rflint import RfLint, ERROR, WARNING 2 | from .parser import RobotFactory, SuiteFile, ResourceFile, Testcase, Keyword 3 | from .common import SuiteRule, ResourceRule, TestRule, KeywordRule, GeneralRule 4 | from .version import __version__ 5 | -------------------------------------------------------------------------------- /test_data/acceptance/nodoc.robot: -------------------------------------------------------------------------------- 1 | # This suite has no embedded documentation, for testing 2 | # the documentation rules 3 | 4 | *** Test Cases *** 5 | | Test case #1 6 | | | no operation 7 | 8 | *** Keywords *** 9 | | Keyword #1 10 | | | no operation 11 | -------------------------------------------------------------------------------- /rflint/exceptions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file defines custom exceptions used by robotframework-lint 3 | 4 | ''' 5 | 6 | class UnknownRuleException(Exception): 7 | def __init__(self, rulename): 8 | super(UnknownRuleException, self).__init__("unknown rule: '%s'" % rulename) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | # command to install dependencies 6 | install: 7 | - "pip install -r requirements-dev.txt" 8 | - "python setup.py install" 9 | # command to run tests 10 | script: 11 | - pybot tests 12 | - flake8 rflint tests 13 | -------------------------------------------------------------------------------- /rflint/parser/__init__.py: -------------------------------------------------------------------------------- 1 | from .parser import (SuiteFolder, ResourceFile, SuiteFile, RobotFactory, 2 | Testcase, Keyword, Row, Statement, TestcaseTable, KeywordTable) 3 | from .tables import DefaultTable, SettingTable, UnknownTable, VariableTable, MetadataTable, RobotTable 4 | 5 | -------------------------------------------------------------------------------- /test_data/acceptance/issue-31.py: -------------------------------------------------------------------------------- 1 | from rflint.common import GeneralRule 2 | 3 | class Issue31(GeneralRule): 4 | def apply(self,robot_file): 5 | message = "the type is %s" % robot_file.type 6 | linenumber = 0 7 | self.report(robot_file, message, linenumber+1, 0) 8 | 9 | 10 | -------------------------------------------------------------------------------- /test_data/acceptance/issue-24.robot: -------------------------------------------------------------------------------- 1 | *** Test case post *** 2 | | Whatever 3 | | | No operation 4 | 5 | *** pre Test case *** 6 | | Whatever 7 | | | No operation 8 | 9 | *** Keyword post *** 10 | | Whatever 11 | | | No operation 12 | 13 | *** pre Keywords *** 14 | | Whatever 15 | | | No operation 16 | 17 | -------------------------------------------------------------------------------- /test_data/acceptance/issue-30.py: -------------------------------------------------------------------------------- 1 | from rflint.common import ResourceRule 2 | 3 | class Issue30(ResourceRule): 4 | def configure(self, value): 5 | self.value = value 6 | 7 | def apply(self,resource): 8 | message = "the configured value is %s" % self.value 9 | self.report(resource, message, 0, 0) 10 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/PeriodInTestName.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | This suite has a test with a period in the name 4 | | ... | to trigger the PeriodInTestName rule. 5 | 6 | *** Test cases *** 7 | | Test.1 8 | | | [Documentation] | example test; should trigger PeriodInTestName rule 9 | | | No operation 10 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/DuplicateKeywordNames_Data.robot: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | | Keyword 1 3 | | | No operation 4 | 5 | | Keyword 2 6 | | | No operation 7 | 8 | *** Test cases *** 9 | | Testcase 1 10 | | | Keyword 1 11 | 12 | *** Keywords *** 13 | | Keyword 1 14 | | | # this shoud throw a duplicate keyword error 15 | | | No operation 16 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/DuplicateTestNames_Data.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | | Test case 1 3 | | | No operation 4 | 5 | | Test case 2 6 | | | No operation 7 | 8 | *** Keywords *** 9 | | Keyword 1 10 | | | No operation 11 | 12 | *** Test Cases *** 13 | | Test case 1 14 | | | # this shoud throw a duplicate keyword error 15 | | | No operation 16 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | ## How to run smoke tests: 2 | 3 | From the root of the repository, issue the following command: 4 | 5 | robot -A tests/conf/smoke.args 6 | 7 | ## How to run the full suite: 8 | 9 | robot -A tests/conf/default.args tests 10 | 11 | ## Test results 12 | 13 | The above will put the results in tests/results (eg: tests/results/log.html, etc.) 14 | -------------------------------------------------------------------------------- /test_data/acceptance/issue-41.robot: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | A keyword with multiple statements, each with a comment 3 | Step 1 # a comment 4 | Step 2 # a comment 5 | 6 | *** Test Cases *** 7 | A test case with multiple statements, each with a comment 8 | Step 1 # step 1 9 | Step 2 # step 2 10 | Step 3 # step 3 11 | Step 4 # step 4 12 | 13 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/DuplicateSettings_Data.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation Having two documentation sections is illegal. 3 | ... Use continuation lines for multiple lines. 4 | Documentation Error here. 5 | 6 | # Several Library settings is okay 7 | Library DateTime 8 | Library Collections 9 | 10 | *** Test Cases *** 11 | Test 1 12 | No operation 13 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/TooFewKeywordSteps_Data.robot: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | | Keyword with zero steps 3 | | | [Documentation] | This keyword has zero steps 4 | 5 | | Keyword with one step 6 | | | [Documentation] | This keyword has one step 7 | | | log | Hello, world 8 | 9 | | Keyword with two steps 10 | | | [Documentation] | This keyword has two steps 11 | | | log | Hello 12 | | | log | world 13 | 14 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/TooFewTestSteps_Data.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | | Test case with zero steps 3 | | | [Documentation] | This test case has zero steps 4 | 5 | | Test case with one step 6 | | | [Documentation] | This test case has one step 7 | | | log | Hello, world 8 | 9 | | Test case with two steps 10 | | | [Documentation] | This test case has two steps 11 | | | log | Hello 12 | | | log | world 13 | 14 | -------------------------------------------------------------------------------- /rflint/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .rflint import RfLint 3 | 4 | 5 | def main(args=None): 6 | try: 7 | app = RfLint() 8 | result = app.run(args) 9 | return result 10 | 11 | except Exception as e: 12 | sys.stderr.write(str(e) + "\n") 13 | return 1 14 | 15 | if __name__ == "__main__": 16 | if len(sys.argv) == 1: 17 | sys.argv.append("--help") 18 | sys.exit(main(sys.argv[1:])) 19 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/PeriodInSuiteName.foo.robot: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # N.B. a test case is required so that this file is interpreted 4 | # as a suite rather than resource file 5 | 6 | *** Settings *** 7 | | Documentation 8 | | ... | This suite should have a period in its name, which will 9 | | ... | trigger the PeriodInSuiteName rule. 10 | 11 | *** Test cases *** 12 | | Test case #1 13 | | | [Documentation] | example test 14 | | | No operation 15 | -------------------------------------------------------------------------------- /tests/conf/default.args: -------------------------------------------------------------------------------- 1 | # default arguments; any other argument files should include 2 | # this file. 3 | --variable BROWSER:chrome 4 | --variable KEYWORD_DIR:tests/keywords 5 | 6 | # put all test results under the tests folder 7 | --outputdir tests/results 8 | 9 | --exclude in-progress 10 | # tests tagged with "test-data" are files used as 11 | # inputs for some of the tests. We don't want to run 12 | # them as actual tests 13 | --exclude test-data 14 | 15 | --tagstatexclude issue-* 16 | -------------------------------------------------------------------------------- /test_data/acceptance/customRules.py: -------------------------------------------------------------------------------- 1 | from rflint.common import TestRule, KeywordRule, SuiteRule, GeneralRule 2 | 3 | class CustomTestRule(TestRule): 4 | def apply(self, testcase): 5 | pass 6 | 7 | class CustomSuiteRule(SuiteRule): 8 | def apply(self, suite): 9 | pass 10 | 11 | class CustomGeneralRule(GeneralRule): 12 | def apply(self, suite): 13 | pass 14 | 15 | class CustomKeywordRule(KeywordRule): 16 | def apply(self, keyword): 17 | pass 18 | 19 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/InvalidTableInResource_Data.robot: -------------------------------------------------------------------------------- 1 | ## Test data for the rule InvalidTable 2 | ## 3 | 4 | # these should fail the rule 5 | 6 | * 7 | * * 8 | *** 9 | * Key word 10 | 11 | # these should be valid: 12 | 13 | * Setting 14 | * Setting 15 | * Setting * 16 | ** Setting 17 | | *** Setting *** 18 | *** Setting *** 19 | *** Setting *** 20 | *** Settings *** 21 | *** Comment *** 22 | *** Comments *** 23 | *** Metadata *** 24 | *** Variable *** 25 | *** Variables *** 26 | *** Keyword *** 27 | *** Keywords *** 28 | *** User Keyword *** 29 | *** User Keywords *** 30 | 31 | # these should fail the rule 32 | 33 | *** bogus *** 34 | 35 | 36 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/TooManyTestSteps.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | This suite has a test with too many test steps 4 | | ... | to trigger the TooManyTestSteps rule. 5 | 6 | *** Test cases *** 7 | | Test with too many steps 8 | | | [Documentation] | example test; should trigger TooManyTestSteps rule 9 | | | No operation 10 | | | No operation 11 | | | No operation 12 | | | No operation 13 | | | No operation 14 | | | No operation 15 | | | No operation 16 | | | No operation 17 | | | No operation 18 | | | No operation 19 | | | No operation 20 | | | No operation 21 | | | No operation 22 | | | No operation 23 | | | No operation 24 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/LineTooLong_Data.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | This suite should trigger two LineTooLong errors 3 | 4 | *** Test Cases *** 5 | | My test case 6 | | | [Documentation] | This test case contains some long lines. 7 | | | 99 characters total | 123456789012345678901234567890123456789012345678901234567890123456789012 8 | | | 100 characters total | 1234567890123456789012345678901234567890123456789012345678901234567890123 9 | | | 101 characters total | 12345678901234567890123456789012345678901234567890123456789012345678901234 10 | | | 102 characters total | 123456789012345678901234567890123456789012345678901234567890123456789012345 11 | 12 | -------------------------------------------------------------------------------- /tests/unit/UnitTestResources.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Resources used by the rflint unit tests 3 | | Library | OperatingSystem 4 | 5 | *** Variables *** 6 | | ${test_data} | test_data/pipes.robot 7 | | ${ROOT} | ${CURDIR}/../.. 8 | 9 | *** Keywords *** 10 | | Parse a robot file and save as a suite variable 11 | | | [Documentation] 12 | | | ... | Parse a robot file and save it as a suite variable 13 | | | 14 | | | # Make sure we're picking up the local rflint! 15 | | | Evaluate | $root not in sys.path and sys.path.insert(0, $root) | sys 16 | | | 17 | | | # parse the file 18 | | | ${rf}= | Evaluate | rflint.RobotFactory($test_data) | rflint 19 | | | Set suite variable | ${rf} 20 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/InvalidTable_Data.robot: -------------------------------------------------------------------------------- 1 | ## Test data for the rule InvalidTable 2 | ## 3 | 4 | # these should fail the rule 5 | 6 | * 7 | * * 8 | *** 9 | *** Testcase *** 10 | * Key word 11 | 12 | # these should be valid: 13 | 14 | * Setting 15 | * Setting 16 | * Setting * 17 | ** Setting 18 | | *** Setting *** 19 | *** Setting *** 20 | *** Setting *** 21 | *** Settings *** 22 | *** Comment *** 23 | *** Comments *** 24 | *** Metadata *** 25 | *** Cases *** 26 | *** Test Case *** 27 | *** Test Cases *** 28 | *** Variable *** 29 | *** Variables *** 30 | *** Keyword *** 31 | *** Keywords *** 32 | *** User Keyword *** 33 | *** User Keywords *** 34 | 35 | # these should fail the rule 36 | 37 | *** bogus *** 38 | 39 | 40 | -------------------------------------------------------------------------------- /rflint/parser/rfkeyword.py: -------------------------------------------------------------------------------- 1 | # Does this need to be in its own file, or can I combine 2 | # testcase and keyword into one file? 3 | from .common import RobotStatements 4 | 5 | class Keyword(RobotStatements): 6 | '''A robotframework keyword 7 | 8 | A keyword is identical to a testcase in almost all respects 9 | except for some of the metadata it supports (which this definition 10 | doesn't (yet) account for...). 11 | ''' 12 | def __init__(self, parent, linenumber, name): 13 | RobotStatements.__init__(self) 14 | self.linenumber = linenumber 15 | self.name = name 16 | self.rows = [] 17 | self.parent = parent 18 | 19 | def __repr__(self): 20 | # should this return the fully qualified name? 21 | return "" % self.name 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Django stuff: 41 | *.log 42 | *.pot 43 | 44 | # Sphinx documentation 45 | docs/_build/ 46 | 47 | # emacs crud 48 | \#*\# 49 | \.\#* 50 | *.elc 51 | *~ 52 | 53 | # robot logs. 54 | log.html 55 | report.html 56 | output.xml 57 | selenium-screenshot-*.png 58 | 59 | # mac crud 60 | .DS_Store 61 | 62 | -------------------------------------------------------------------------------- /tests/acceptance/issues/issue-30.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | This suite includes tests for specific issues in the issue tracker 4 | | # 5 | | Resource | ../SharedKeywords.robot 6 | | # 7 | | Test Teardown 8 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 9 | | ... | log | ${result.stdout}\n${result.stderr} 10 | 11 | *** Test Cases *** 12 | | Issue 30 13 | | | [tags] | issue-30 14 | | | [Documentation] 15 | | | ... | Verify that you can configure a custom ResourceRule 16 | | | 17 | | | Run rf-lint with the following options: 18 | | | ... | --ignore | all 19 | | | ... | --rulefile | test_data/acceptance/issue-30.py 20 | | | ... | --warning | Issue30 21 | | | ... | --configure | Issue30:42 22 | | | ... | test_data/keywords.robot 23 | | | 24 | | | Stderr should be | ${EMPTY} 25 | | | Stdout should be 26 | | | ... | + test_data/keywords.robot 27 | | | ... | W: 0, 0: the configured value is 42 (Issue30) 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/acceptance/rules/DuplicateTestNames.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the suite rule 'DuplicateTestNames' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify all defined tests have unique names 11 | | | [Documentation] 12 | | | ... | Verify that all defined tests have unique names. 13 | | | ... | If a test name is duplicated, the rule should flag 14 | | | ... | the duplicates but not the original. 15 | | | 16 | | | [Setup] | Run rf-lint with the following options: 17 | | | ... | --no-filename 18 | | | ... | --ignore | all 19 | | | ... | --error | DuplicateTestNames 20 | | | ... | test_data/acceptance/rules/DuplicateTestNames_Data.robot 21 | | | 22 | | | rflint return code should be | 1 23 | | | rflint should report 1 errors 24 | | | rflint should report 0 warnings 25 | -------------------------------------------------------------------------------- /tests/acceptance/rules/DuplicateKeywordNames.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the suite rule 'DuplicateKeywordNames' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify all defined keywords have unique names 11 | | | [Documentation] 12 | | | ... | Verify that all defined keywords have unique names. 13 | | | ... | If a keyword name is duplicated, the rule should flag 14 | | | ... | the duplicates but not the original. 15 | | | 16 | | | [Setup] | Run rf-lint with the following options: 17 | | | ... | --no-filename 18 | | | ... | --ignore | all 19 | | | ... | --error | DuplicateKeywordNames 20 | | | ... | test_data/acceptance/rules/DuplicateKeywordNames_Data.robot 21 | | | 22 | | | rflint return code should be | 1 23 | | | rflint should report 1 errors 24 | | | rflint should report 0 warnings 25 | -------------------------------------------------------------------------------- /tests/acceptance/rules/DuplicateSettings.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the suite rule 'DuplicateSettings' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify duplicate settings raise an error 11 | | | [Documentation] 12 | | | ... | Verify duplicate settings raise an error 13 | | | 14 | | | [Setup] | Run rf-lint with the following options: 15 | | | ... | --no-filename 16 | | | ... | --ignore | all 17 | | | ... | --error | DuplicateSettingsInSuite 18 | | | ... | test_data/acceptance/rules/DuplicateSettings_Data.robot 19 | | | 20 | | | Stderr should be | ${EMPTY} 21 | | | Stdout should be 22 | | | ... | E: 4, 0: Setting 'Documentation' used multiple times (previously used line 2) (DuplicateSettingsInSuite) 23 | | | 24 | | | rflint return code should be | 1 25 | | | rflint should report 1 errors 26 | | | rflint should report 0 warnings 27 | -------------------------------------------------------------------------------- /tests/acceptance/rules/TrailingWhitespace.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the general rule 'TrailingWhitespace' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify TrailingWhitespace is triggered properly 11 | | | [tags] | issue-37 12 | | | [Documentation] | 13 | | | ... | Verify that TrailingWhitespace rule is triggered 14 | | | ... | for lines that have trailing whitespace 15 | | | 16 | | | [Setup] 17 | | | ... | Run rf-lint with the following options: 18 | | | ... | --ignore | all 19 | | | ... | --warning | TrailingWhitespace 20 | | | ... | test_data/acceptance/issue-37.robot 21 | | | 22 | | | Stderr should be | ${EMPTY} 23 | | | Stdout should be 24 | | | ... | + test_data/acceptance/issue-37.robot 25 | | | ... | W: 3, 0: Line has trailing whitespace (TrailingWhitespace) 26 | | | ... | W: 4, 0: Line has trailing whitespace (TrailingWhitespace) 27 | -------------------------------------------------------------------------------- /tests/acceptance/issues/issue-31.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | This suite includes tests for specific issues in the issue tracker 4 | | # 5 | | Resource | ../SharedKeywords.robot 6 | | # 7 | | Test Teardown 8 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 9 | | ... | log | ${result.stdout}\n${result.stderr} 10 | 11 | *** Test Cases *** 12 | | Issue 31 13 | | | [tags] | issue-31 14 | | | [Documentation] | 15 | | | ... | Verify that GeneralRule is passed an object with a type attribute 16 | | | 17 | | | Run rf-lint with the following options: 18 | | | ... | --ignore | all 19 | | | ... | --rulefile | test_data/acceptance/issue-31.py 20 | | | ... | --warning | Issue31 21 | | | ... | test_data/acceptance/nodoc.robot 22 | | | ... | test_data/keywords.robot 23 | | | 24 | | | Stderr should be | ${EMPTY} 25 | | | Stdout should be 26 | | | ... | + test_data/acceptance/nodoc.robot 27 | | | ... | W: 1, 0: the type is suite (Issue31) 28 | | | ... | + test_data/keywords.robot 29 | | | ... | W: 1, 0: the type is resource (Issue31) 30 | -------------------------------------------------------------------------------- /rflint/parser/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import time 4 | import re 5 | 6 | class Matcher(object): 7 | '''A convenience class for regular expression matching 8 | 9 | Example: 10 | match = Matcher(re.IGNORECASE) 11 | if match(pattern1, string): 12 | print match.group(1) 13 | elif match(pattern2, string): 14 | print match.group(1) 15 | ... 16 | ''' 17 | 18 | def __init__(self, flags=None): 19 | self.flags = flags 20 | 21 | def __call__(self, pattern, string, flags=None): 22 | if flags is None: 23 | self.result = re.match(pattern, string, flags=self.flags) 24 | else: 25 | self.result = re.match(pattern, string) 26 | return self.result 27 | 28 | def __getattr__(self, attr): 29 | return getattr(self.result, attr) 30 | 31 | 32 | def timeit(func): 33 | def wrapper(*arg, **kw): 34 | '''source: http://www.daniweb.com/code/snippet368.html''' 35 | t1 = time.time() 36 | res = func(*arg, **kw) 37 | t2 = time.time() 38 | print(func.__name__, ":", int(1000*(t2-t1)), "ms") 39 | return res 40 | return wrapper 41 | 42 | -------------------------------------------------------------------------------- /rflint/rules/resourceRules.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from rflint.common import ResourceRule 4 | 5 | class InvalidTableInResource(ResourceRule): 6 | '''Verify that there are no invalid table headers''' 7 | valid_tables_re = None 8 | default_robot_level = "robot3" 9 | 10 | def configure(self, robot_level): 11 | valid_tables = ['comments?', 'settings?', 'keywords?', 'variables?'] 12 | if robot_level == "robot2": 13 | valid_tables += ['metadata', 'user keywords?'] 14 | self.valid_tables_re = re.compile('^(' + '|'.join(valid_tables) + ')$', 15 | re.I) 16 | 17 | if robot_level == "robot2": 18 | valid_tables += ['metadata', 'user keyword'] 19 | self.valid_tables_re = re.compile('^(' + '|'.join(valid_tables) + ')$', 20 | re.I) 21 | 22 | def apply(self, resource): 23 | if not self.valid_tables_re: 24 | self.configure(self.default_robot_level) 25 | for table in resource.tables: 26 | if not self.valid_tables_re.match(table.name): 27 | self.report(resource, "Unknown table name '%s'" % table.name, 28 | table.linenumber) 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | # .DEFAULT_GOAL := help 3 | 4 | clean: clean-build clean-pyc clean-test 5 | 6 | test: test-py2 test-py3 7 | python3 -m robot.rebot \ 8 | --outputdir tests/results \ 9 | tests/results/py2/output.xml \ 10 | tests/results/py3/output.xml \ 11 | 12 | test-py3: 13 | python3 -m robot \ 14 | --argumentfile tests/conf/default.args \ 15 | --name "Python 3 Tests" \ 16 | --outputdir tests/results/py3 \ 17 | --variable BROWSER:chrome \ 18 | tests 19 | 20 | test-py2: 21 | python2 -m robot \ 22 | --argumentfile tests/conf/default.args \ 23 | --name "Python 2 Tests" \ 24 | --outputdir tests/results/py2 \ 25 | --variable BROWSER:chrome \ 26 | tests 27 | 28 | clean-build: 29 | rm -rf build/ 30 | rm -rf dist/ 31 | rm -rf .eggs/ 32 | find . -name '*.egg-info' -exec rm -rf {} + 33 | find . -name '*.egg' -exec rm -f {} + 34 | 35 | clean-pyc: 36 | find . -name '*.pyc' -exec rm -f {} + 37 | find . -name '*.pyo' -exec rm -f {} + 38 | find . -name '*~' -exec rm -f {} + 39 | find . -name '__pycache__' -exec rm -rf {} + 40 | 41 | clean-test: 42 | rm -rf tests/results/* 43 | 44 | release: clean dist 45 | twine upload dist/* 46 | 47 | dist: clean 48 | python setup.py sdist 49 | ls -l dist 50 | -------------------------------------------------------------------------------- /tests/acceptance/rules/PeriodInSuiteName.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the suite rule 'PeriodInSuiteName' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Suite WITH a period in the name 11 | | | [Documentation] 12 | | | ... | Verify that a suite with a period in the name triggers the rule. 13 | | | 14 | | | [Setup] | Run rf-lint with the following options: 15 | | | ... | --no-filename 16 | | | ... | --ignore | all 17 | | | ... | --error | PeriodInSuiteName 18 | | | ... | test_data/acceptance/rules/PeriodInSuiteName.foo.robot 19 | | | 20 | | | rflint return code should be | 1 21 | | | rflint should report 1 errors 22 | | | rflint should report 0 warnings 23 | 24 | | Suite WITHOUT a period in the name 25 | | | [Documentation] 26 | | | ... | Verify that a suite without a period in the name does NOT trigger the rule. 27 | | | 28 | | | [Setup] | Run rf-lint with the following options: 29 | | | ... | --no-filename 30 | | | ... | --ignore | all 31 | | | ... | --error | PeriodInSuiteName 32 | | | ... | ${SUITE SOURCE} | # use this file as input 33 | | | 34 | | | rflint return code should be | 0 35 | | | rflint should report 0 errors 36 | | | rflint should report 0 warnings 37 | -------------------------------------------------------------------------------- /tests/acceptance/rules/PeriodInTestName.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the testcase rule 'PeriodInTestName' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Testcase WITH a period in the name 11 | | | [Documentation] 12 | | | ... | Verify that a testcase with a period in the name triggers the rule. 13 | | | 14 | | | [Setup] | Run rf-lint with the following options: 15 | | | ... | --no-filename 16 | | | ... | --ignore | all 17 | | | ... | --error | PeriodInTestName 18 | | | ... | test_data/acceptance/rules/PeriodInTestName.robot 19 | | | 20 | | | rflint return code should be | 1 21 | | | rflint should report 1 errors 22 | | | rflint should report 0 warnings 23 | 24 | | Testcase WITHOUT a period in the name 25 | | | [Documentation] 26 | | | ... | Verify that a testcase without a period in the name does NOT trigger the rule. 27 | | | 28 | | | [Setup] | Run rf-lint with the following options: 29 | | | ... | --no-filename 30 | | | ... | --ignore | all 31 | | | ... | --error | PeriodInTestName 32 | | | ... | ${SUITE SOURCE} | # use this file as input 33 | | | 34 | | | rflint return code should be | 0 35 | | | rflint should report 0 errors 36 | | | rflint should report 0 warnings 37 | -------------------------------------------------------------------------------- /tests/acceptance/rules/LineTooLong.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the rule 'LineTooLong' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify all long lines are detected 11 | | | [Documentation] 12 | | | ... | Verify that all long lines in the file are detected. 13 | | | 14 | | | [Setup] | Run rf-lint with the following options: 15 | | | ... | --no-filename 16 | | | ... | --ignore | all 17 | | | ... | --error | LineTooLong 18 | | | ... | test_data/acceptance/rules/LineTooLong_Data.robot 19 | | | 20 | | | rflint return code should be | 2 21 | | | rflint should report 2 errors 22 | | | rflint should report 0 warnings 23 | 24 | | Verify that the proper error message is returned 25 | | | [Documentation] 26 | | | ... | Verify that LineTooLong returns the expected message 27 | | | ... | for every error 28 | | | 29 | | | [Setup] | Run rf-lint with the following options: 30 | | | ... | --no-filename 31 | | | ... | --ignore | all 32 | | | ... | --error | LineTooLong 33 | | | ... | test_data/acceptance/rules/LineTooLong_Data.robot 34 | | | 35 | | | Output should contain 36 | | | ... | E: 9, 100: Line is too long (exceeds 100 characters) (LineTooLong) 37 | | | ... | E: 10, 100: Line is too long (exceeds 100 characters) (LineTooLong) 38 | -------------------------------------------------------------------------------- /tests/acceptance/issues/issue-24.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | This suite includes tests for specific issues in the issue tracker 4 | | # 5 | | Resource | ../SharedKeywords.robot 6 | | # 7 | | Test Teardown 8 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 9 | | ... | log | ${result.stdout}\n${result.stderr} 10 | 11 | *** Test Cases *** 12 | | Issue 24 13 | | | [tags] | issue-24 14 | | | [Documentation] 15 | | | ... | Verify that InvalidTable does anchored match 16 | | | ... | 17 | | | ... | This issue resulted in strings like *** Keywordsz *** 18 | | | ... | being considered valid, because we searched for "Keywords". 19 | | | ... | The fix was to do an anchored search (eg: ^Keywords$) 20 | | | ... | (though the actual regex is a bit more complicated) 21 | | | 22 | | | Run rf-lint with the following options: 23 | | | ... | --ignore | all 24 | | | ... | --error | InvalidTable 25 | | | ... | test_data/acceptance/issue-24.robot 26 | | | 27 | | | rflint return code should be | 4 28 | | | Stderr should be | ${EMPTY} 29 | | | Stdout should be 30 | | | ... | + test_data/acceptance/issue-24.robot 31 | | | ... | E: 1, 0: Unknown table name 'Test case post' (InvalidTable) 32 | | | ... | E: 5, 0: Unknown table name 'pre Test case' (InvalidTable) 33 | | | ... | E: 9, 0: Unknown table name 'Keyword post' (InvalidTable) 34 | | | ... | E: 13, 0: Unknown table name 'pre Keywords' (InvalidTable) 35 | -------------------------------------------------------------------------------- /tests/acceptance/rules/TooFewTestSteps.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the testcase rule 'TooFewTestSteps' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify TooFewTestSteps default catches less than two steps 11 | | | [Documentation] 12 | | | ... | Verify that TooFewTestSteps raises errors with less than two steps 13 | | | 14 | | | [Setup] | Run rf-lint with the following options: 15 | | | ... | --no-filename 16 | | | ... | --ignore | all 17 | | | ... | --error | TooFewTestSteps 18 | | | ... | test_data/acceptance/rules/TooFewTestSteps_Data.robot 19 | | | 20 | | | rflint return code should be | 2 21 | | | rflint should report 2 errors 22 | | | rflint should report 0 warnings 23 | 24 | | Verify TooFewTestSteps configurability 25 | | | [Documentation] 26 | | | ... | Verify that TooFewTestSteps can be configured 27 | | | 28 | | | [Setup] | Run rf-lint with the following options: 29 | | | ... | --no-filename 30 | | | ... | --ignore | all 31 | | | ... | --configure | TooFewTestSteps:0 32 | | | ... | --error | TooFewTestSteps 33 | | | ... | test_data/acceptance/rules/TooFewTestSteps_Data.robot 34 | | | 35 | | | rflint return code should be | 0 36 | | | rflint should report 0 errors 37 | | | rflint should report 0 warnings 38 | 39 | -------------------------------------------------------------------------------- /tests/acceptance/issues/issue-41.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | Issue 41 - TooFewSteps failing when a statement contains a comment anywhere 4 | | # 5 | | Resource | ../SharedKeywords.robot 6 | | # 7 | | Test Teardown 8 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 9 | | ... | log | ${result.stdout}\n${result.stderr} 10 | 11 | *** Test Cases *** 12 | | Issue 41 - TooFewKeywordSteps 13 | | | 14 | | | [tags] | issue-41 15 | | | [Documentation] 16 | | | ... | Verify TooFewKeywordSteps works when every step has a comment 17 | | | 18 | | | Run rf-lint with the following options: 19 | | | ... | --ignore | all 20 | | | ... | --warning | TooFewKeywordSteps 21 | | | ... | --configure | TooFewKeywordSteps:1 22 | | | ... | test_data/acceptance/issue-41.robot 23 | | | 24 | | | rflint return code should be | 0 25 | | | Stderr should be | ${EMPTY} 26 | | | Stdout should be | ${EMPTY} 27 | 28 | | Issue 41 - TooFewTestSteps 29 | | | 30 | | | [tags] | issue-41 31 | | | [Documentation] 32 | | | ... | Verify TooFewTestSteps works when every step has a comment 33 | | | 34 | | | Run rf-lint with the following options: 35 | | | ... | --ignore | all 36 | | | ... | --warning | TooFewTestSteps 37 | | | ... | --configure | TooFewTestSteps:1 38 | | | ... | test_data/acceptance/issue-41.robot 39 | | | 40 | | | rflint return code should be | 0 41 | | | Stderr should be | ${EMPTY} 42 | | | Stdout should be | ${EMPTY} 43 | -------------------------------------------------------------------------------- /tests/acceptance/rules/TooFewKeywordSteps.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the testcase rule 'TooFewTestSteps' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify TooFewKeywordSteps default catches less than two steps 11 | | | [Documentation] 12 | | | ... | Verify that TooFewKeywordSteps raises errors with less than two steps 13 | | | 14 | | | [Setup] | Run rf-lint with the following options: 15 | | | ... | --no-filename 16 | | | ... | --ignore | all 17 | | | ... | --error | TooFewKeywordSteps 18 | | | ... | test_data/acceptance/rules/TooFewKeywordSteps_Data.robot 19 | | | 20 | | | rflint return code should be | 2 21 | | | rflint should report 2 errors 22 | | | rflint should report 0 warnings 23 | 24 | | Verify TooFewKeywordSteps configurability 25 | | | [Documentation] 26 | | | ... | Verify that TooFewKeywordSteps can be configured 27 | | | 28 | | | [Setup] | Run rf-lint with the following options: 29 | | | ... | --no-filename 30 | | | ... | --ignore | all 31 | | | ... | --configure | TooFewKeywordSteps:0 32 | | | ... | --error | TooFewKeywordSteps 33 | | | ... | test_data/acceptance/rules/TooFewKeywordSteps_Data.robot 34 | | | 35 | | | rflint return code should be | 0 36 | | | rflint should report 0 errors 37 | | | rflint should report 0 warnings 38 | 39 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/TooManyTestCases.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | This suite has too many tests to trigger the TooManyTestCases rule. 4 | 5 | *** Test cases *** 6 | | Test 1 7 | | | [Documentation] | example test; should trigger TooManyTestCases rule 8 | | | No operation 9 | 10 | | Test 2 11 | | | [Documentation] | example test; should trigger TooManyTestCases rule 12 | | | No operation 13 | 14 | | Test 3 15 | | | [Documentation] | example test; should trigger TooManyTestCases rule 16 | | | No operation 17 | 18 | | Test 4 19 | | | [Documentation] | example test; should trigger TooManyTestCases rule 20 | | | No operation 21 | 22 | | Test 5 23 | | | [Documentation] | example test; should trigger TooManyTestCases rule 24 | | | No operation 25 | 26 | | Test 6 27 | | | [Documentation] | example test; should trigger TooManyTestCases rule 28 | | | No operation 29 | 30 | | Test 7 31 | | | [Documentation] | example test; should trigger TooManyTestCases rule 32 | | | No operation 33 | 34 | | Test 8 35 | | | [Documentation] | example test; should trigger TooManyTestCases rule 36 | | | No operation 37 | 38 | | Test 9 39 | | | [Documentation] | example test; should trigger TooManyTestCases rule 40 | | | No operation 41 | 42 | | Test 10 43 | | | [Documentation] | example test; should trigger TooManyTestCases rule 44 | | | No operation 45 | 46 | | Test 11 47 | | | [Documentation] | example test; should trigger TooManyTestCases rule 48 | | | No operation 49 | -------------------------------------------------------------------------------- /rflint/parser/testcase.py: -------------------------------------------------------------------------------- 1 | from .tables import SettingTable 2 | from .common import Row, Statement, RobotStatements 3 | import re 4 | 5 | 6 | class Testcase(RobotStatements): 7 | def __init__(self, parent, linenumber, name): 8 | RobotStatements.__init__(self) 9 | self.linenumber = linenumber 10 | self.name = name 11 | self.rows = [] 12 | self.parent = parent 13 | 14 | @property 15 | def is_templated(self): 16 | """Return True if the test is part of a suite that uses a Test Template""" 17 | for table in self.parent.tables: 18 | if isinstance(table, SettingTable): 19 | for row in table.rows: 20 | if row[0].lower() == "test template": 21 | return True 22 | return False 23 | 24 | # this is great, except that we don't return the line number 25 | # or character position of each tag. The linter needs that. :-( 26 | @property 27 | def tags(self): 28 | tags = [] 29 | for statement in self.statements: 30 | if len(statement) > 2 and statement[1].lower() == "[tags]": 31 | for tag in statement[2:]: 32 | if tag.startswith("#"): 33 | # the start of a comment, so skip rest of the line 34 | break 35 | else: 36 | tags.append(tag) 37 | break 38 | return tags 39 | 40 | def __repr__(self): 41 | # should this return the fully qualified name? 42 | return "" % self.name 43 | -------------------------------------------------------------------------------- /tests/acceptance/unicode.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | Tests related to the handling of unicode data 4 | | 5 | | Library | OperatingSystem 6 | | Library | Process 7 | | Library | SharedKeywords.py 8 | | Resource | SharedKeywords.robot 9 | | Force Tags | smoke 10 | | 11 | | Test Teardown 12 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 13 | | ... | log | ${result.stdout}\n${result.stderr} 14 | 15 | *** Test Cases *** 16 | 17 | | Testcase with unicode in the name 18 | | | [Documentation] 19 | | | ... | Verify we can properly report issues in test cases with unicode 20 | | | ... | in the test case name 21 | | | [tags] | issue-35 | unicode 22 | 23 | | | run rf-lint with the following options: 24 | | | ... | --ignore | all 25 | | | ... | --warning | PeriodInTestName 26 | | | ... | test_data/acceptance/issue-35.robot 27 | | | rflint return code should be | 0 28 | | | rflint should report 1 warnings 29 | | | Stdout should be 30 | | | ... | + test_data/acceptance/issue-35.robot 31 | | | ... | W: 2, 0: '.' in testcase name 'Пример тест-кейса.' (PeriodInTestName) 32 | 33 | | Test suite with unicode in the file name 34 | | | [Documentation] 35 | | | ... | Verify that we can process files with unicode in the file name 36 | | | [tags] | unicode 37 | 38 | | | run rf-lint with the following options: 39 | | | ... | --ignore | all 40 | | | ... | --warning | RequireSuiteDocumentation 41 | | | ... | test_data/acceptance/名稱與unicode.robot 42 | | | rflint return code should be | 0 43 | | | rflint should report 1 warnings 44 | | | stdout should be 45 | | | ... | + test_data/acceptance/名稱與unicode.robot 46 | | | ... | W: 1, 0: No suite documentation (RequireSuiteDocumentation) 47 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # N.B. to push a new version to PyPi, update the version number 2 | # in rflint/version.py and then run 'python setup.py sdist upload' 3 | 4 | from setuptools import setup 5 | 6 | filename = 'rflint/version.py' 7 | exec(compile(open(filename, 'rb').read(), filename, 'exec')) 8 | 9 | setup( 10 | name = 'robotframework-lint', 11 | version = __version__, 12 | author = 'Bryan Oakley', 13 | author_email = 'bryan.oakley@gmail.com', 14 | url = 'https://github.com/boakley/robotframework-lint/', 15 | keywords = 'robotframework', 16 | license = 'Apache License 2.0', 17 | description = 'Static analysis tool for robotframework plain text files', 18 | long_description = open('README.md').read(), 19 | zip_safe = False, 20 | include_package_data = True, 21 | install_requires = ['robotframework'], 22 | classifiers = [ 23 | "Development Status :: 5 - Production/Stable", 24 | "License :: OSI Approved :: Apache Software License", 25 | "Operating System :: OS Independent", 26 | "Framework :: Robot Framework", 27 | "Programming Language :: Python", 28 | "Topic :: Software Development :: Testing", 29 | "Topic :: Software Development :: Quality Assurance", 30 | "Intended Audience :: Developers", 31 | "Environment :: Console", 32 | ], 33 | packages =[ 34 | 'rflint', 35 | 'rflint.rules', 36 | 'rflint.parser', 37 | ], 38 | scripts =[], 39 | entry_points={ 40 | 'console_scripts': [ 41 | "rflint = rflint.__main__:main" 42 | ] 43 | } 44 | ) 45 | -------------------------------------------------------------------------------- /tests/unit/keyword.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | A collection of tests for the rflint keyword object 4 | | # 5 | | Library | Collections 6 | | Resource | UnitTestResources.robot 7 | | # 8 | | Suite Setup | Run Keywords 9 | | ... | Parse a robot file and save as a suite variable 10 | | ... | AND | Set suite variable | ${keyword_table} | ${rf.tables[4]} 11 | 12 | *** Test Cases *** 13 | | Keywords table has expected number of keywords 14 | | | [Documentation] 15 | | | ... | Verify that the keyword table has the expected number of keywords 16 | | | 17 | | | Length should be | ${rf.tables[4].keywords} | 2 18 | | | ... | Expected the keyword table to have 2 keywords but it did not. 19 | 20 | | Keywords have the expected number of rows 21 | | | [Documentation] 22 | | | ... | Verify that that each keyword has the expected number of rows 23 | | | 24 | | | [Setup] | Set test variable | ${keyword_table} | ${rf.tables[4]} 25 | | | [Template] | Run keyword and continue on failure 26 | | | Length should be | ${keyword_table.keywords[0].rows} | 6 27 | | | Length should be | ${keyword_table.keywords[1].rows} | 3 28 | 29 | *** Keywords *** 30 | | Verify keyword ${keyword_num} has ${expected} rows 31 | | | [Documentation] 32 | | | ... | Fail if the given keyword doesn't have the correct number of rows 33 | | | 34 | | | ${keyword}= | Set variable | ${keyword_table.keywords[${keyword_num}]} 35 | | | ${actual}= | Get length | ${keyword.rows} 36 | | | Should be equal as numbers | ${actual} | ${expected} 37 | | | ... | Expected '${keyword.name}' to have ${expected} rows but it had ${actual} 38 | | | ... | values=False 39 | | | 40 | | | [Teardown] | Run keyword if | "${Keyword Status}" == "FAIL" 41 | | | ... | log list | ${keyword.rows} 42 | -------------------------------------------------------------------------------- /tests/acceptance/rules/DuplicateVariables.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the suite rule 'DuplicateVariables' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify duplicate variable definitions raise an error 11 | | | [Documentation] 12 | | | ... | Verify duplicate variable definitions raise an error 13 | | | 14 | | | [Setup] | Run rf-lint with the following options: 15 | | | ... | --no-filename 16 | | | ... | --ignore | all 17 | | | ... | --error | DuplicateVariablesInSuite 18 | | | ... | test_data/acceptance/rules/DuplicateVariables_Data.robot 19 | | | 20 | | | Stderr should be | ${EMPTY} 21 | | | Stdout should be 22 | | | ... | E: 3, 0: Variable 'some_var' defined twice, previous definition line 2 (DuplicateVariablesInSuite) 23 | | | ... | E: 4, 0: Variable 'SomeVar' defined twice, previous definition line 2 (DuplicateVariablesInSuite) 24 | 25 | | Verify duplicate variable definitions in a Resource file raise an error 26 | | | [Documentation] 27 | | | ... | Verify duplicate variable definitions in a Resource file raise an error 28 | | | 29 | | | [Setup] | Run rf-lint with the following options: 30 | | | ... | --no-filename 31 | | | ... | --ignore | all 32 | | | ... | --error | DuplicateVariablesInResource 33 | | | ... | test_data/acceptance/rules/DuplicateVariablesInResource_Data.robot 34 | | | 35 | | | Stderr should be | ${EMPTY} 36 | | | Stdout should be 37 | | | ... | E: 3, 0: Variable 'some_var' defined twice, previous definition line 2 (DuplicateVariablesInResource) 38 | | | ... | E: 4, 0: Variable 'SomeVar' defined twice, previous definition line 2 (DuplicateVariablesInResource) 39 | -------------------------------------------------------------------------------- /tests/acceptance/SharedKeywords.py: -------------------------------------------------------------------------------- 1 | from robot.libraries.BuiltIn import BuiltIn 2 | 3 | def create_a_test_suite(filename, *args): 4 | '''Create a test suite file that mirrors the input 5 | 6 | This creates a test suite file in pipe-separated format. 7 | The input needs to include explicit newlines for each line, 8 | and variables must be escaped. 9 | 10 | If you want literal variables or comments, you must escape 11 | the $ and/or # 12 | 13 | Example: 14 | 15 | *** Test Cases *** 16 | | Example test case 17 | | | [Setup] | Create a temporary suite | ${test_suite} 18 | | | ... | *** Test Cases ***\n 19 | | | ... | An example test case\n 20 | | | ... | | [Documentation] | this is a sample testcase\n 21 | | | ... | | ... | \n 22 | | | ... | | ... | blah blah blah\n 23 | | | ... | | log | hello from \${TEST NAME}\n 24 | 25 | ''' 26 | suite = args[0] 27 | # In order to make it as easy as possible to create and read 28 | # the test, we don't require that the caller escape the 29 | # pipes. So, we have to insert literal pipes between each 30 | # argument, and add a leading pipe after each newline. 31 | for arg in args[1:]: 32 | 33 | if suite[-1] != "\n" and suite[-1] != "|": 34 | suite += " " 35 | 36 | if suite[-1] == "\n" and arg.startswith("#"): 37 | suite += arg 38 | 39 | elif arg == "\n" or arg.startswith("*"): 40 | # blank lines and headers get appended as-is 41 | suite += arg 42 | else: 43 | # everything else we add a separator and then the argument 44 | suite += "| " + arg 45 | with open(filename, "w") as f: 46 | f.write(suite) 47 | 48 | return suite 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/acceptance/rules/FileTooLong.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the rule 'FileTooLong' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify a short file passes FileTooLong 11 | | | [Documentation] 12 | | | ... | Verify that a file of reasonable length passes the FileTooLong rule 13 | | | 14 | | | # We know this file isn't too long, so we'll use it for 15 | | | # our test data 16 | | | [Setup] | Run rf-lint with the following options: 17 | | | ... | --no-filename 18 | | | ... | --ignore | all 19 | | | ... | --error | FileTooLong 20 | | | ... | ${SUITE_SOURCE} 21 | | | 22 | | | rflint return code should be | 0 23 | 24 | | Verify the default limit of 300 is caught 25 | | | [Documentation] 26 | | | ... | Verify that FileTooLong gives the expected message 27 | | | ... | for a long file. 28 | | | 29 | | | [Setup] | Run rf-lint with the following options: 30 | | | ... | --no-filename 31 | | | ... | --ignore | all 32 | | | ... | --error | FileTooLong 33 | | | ... | test_data/acceptance/rules/FileTooLong_Data.robot 34 | | | 35 | | | rflint return code should be | 1 36 | | | Output should contain 37 | | | ... | E: 301, 0: File has too many lines (301) (FileTooLong) 38 | 39 | | Verify we can reconfigure the limit 40 | | | [Documentation] 41 | | | ... | Verify that we can reconfigure FileTooLong to accept 42 | | | ... | a different limit 43 | | | 44 | | | [Setup] | Run rf-lint with the following options: 45 | | | ... | --no-filename 46 | | | ... | --ignore | all 47 | | | ... | --error | FileTooLong 48 | | | ... | --configure | FileTooLong:400 49 | | | ... | test_data/acceptance/rules/FileTooLong_Data.robot 50 | | | 51 | | | rflint return code should be | 0 52 | -------------------------------------------------------------------------------- /test_data/pipes.robot: -------------------------------------------------------------------------------- 1 | # This test isn't designed to be run, it's used as an input 2 | # for some unit tests 3 | # 4 | # Note: this test intentionally uses different styles for the 5 | # table headings and there's a test for each style, so modify 6 | # at your own risk. 7 | 8 | *** Settings *** 9 | | # comment in the settings table 10 | | Documentation | A simple test with some basic features 11 | | Force tags | test-data 12 | | Metadata | test format | pipe-separated 13 | | Suite Setup | No operation 14 | | Suite Teardown | No operation 15 | # whole-line comment in the settings table 16 | 17 | ** Variables 18 | | # comment in the variable table 19 | | ${FOO} | this is foo 20 | | @{bar} | one | two | three 21 | # whole-line comment in the variables table 22 | 23 | * Test Cases * 24 | | Test case #1 25 | | # comment in a test case 26 | | | [Documentation] 27 | | | ... | This is the documentation for test case #1 28 | | | [Setup] | No operation 29 | | | [Tags] | tag1 | tag2 | # comment; not a tag 30 | | | No operation | # comment in a statement 31 | | | [Teardown] | No operation 32 | 33 | | Test case #2 34 | | | [Documentation] 35 | | | ... | This is the documentation for test case #2 36 | | | [Tags] | tag1 | tag2 37 | | | No operation 38 | 39 | | Test case #3 40 | | | [Documentation] | make sure we can support whole-line comments 41 | # whole-line comment in a test case, followed by blank line 42 | 43 | | | No operation 44 | 45 | *** Bogus Table 46 | this table should parse even though the table 47 | 48 | * Keywords *** 49 | | Keyword #1 50 | | | [Documentation] 51 | | | ... | This is the documentation for keyword #1 52 | # whole-line comment in a keyword, followed by a blank line 53 | 54 | | | No operation 55 | 56 | | Keyword #2 57 | | | [Documentation] 58 | | | ... | This is the documentation for keyword #2 59 | | | No operation 60 | -------------------------------------------------------------------------------- /rflint/rules/keywordRules.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2014 Bryan Oakley 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | ''' 16 | 17 | from rflint.common import KeywordRule, ERROR 18 | from rflint.parser import SettingTable 19 | 20 | class RequireKeywordDocumentation(KeywordRule): 21 | '''Verify that a keyword has documentation''' 22 | severity=ERROR 23 | 24 | def apply(self, keyword): 25 | for setting in keyword.settings: 26 | if setting[1].lower() == "[documentation]" and len(setting) > 2: 27 | return 28 | 29 | # set the line number to the line immediately after the keyword name 30 | self.report(keyword, "No keyword documentation", keyword.linenumber+1) 31 | 32 | 33 | class TooFewKeywordSteps(KeywordRule): 34 | '''Keywords should have at least a minimum number of steps 35 | 36 | This rule is configurable. The default number of required steps is 2. 37 | ''' 38 | 39 | min_required = 2 40 | 41 | def configure(self, min_required): 42 | self.min_required = int(min_required) 43 | 44 | def apply(self, keyword): 45 | # ignore empty steps 46 | steps = [step for step in keyword.steps if not (len(step) == 1 and not step[0])] 47 | if len(steps) < self.min_required: 48 | msg = "Too few steps (%s) in keyword" % len(steps) 49 | self.report(keyword, msg, keyword.linenumber) 50 | -------------------------------------------------------------------------------- /tests/unit/testcase.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | A collection of tests for the rflint testcase object 4 | | # 5 | | Library | Collections 6 | | Resource | UnitTestResources.robot 7 | | # 8 | | Suite Setup | Run Keywords 9 | | ... | Parse a robot file and save as a suite variable 10 | | ... | AND | Set suite variable | ${testcase_table} | ${rf.tables[2]} 11 | 12 | *** Test Cases *** 13 | | Testcase table has expected number of test cases 14 | | | [Documentation] | Verify that the testcase table has the expected number of rows 15 | | | Length should be | ${testcase_table.testcases} | 3 16 | | | ... | Expected the testcase table to have 3 tests but it did not. 17 | 18 | | Test cases have the expected number of rows 19 | | | [Documentation] 20 | | | ... | Fail if the parsed testcase has the wrong number of rows 21 | | | 22 | | | # N.B. using the template lets all keywords run even when some fail 23 | | | [Template] | Run keyword 24 | | | Verify test case 0 has 8 rows 25 | | | Verify test case 1 has 5 rows 26 | | | Verify test case 2 has 5 rows 27 | 28 | | Testcase.tags ignores comments 29 | | | [Documentation] | Verify that comments aren't included in the list of tags for a test 30 | | | [tags] | issue-61 31 | | | ${testcase}= | Set variable | ${testcase_table.testcases[0]} 32 | | | log to console | \ntags: ${testcase.tags} 33 | | | ${expected}= | Create List | tag1 | tag2 34 | | | Should be equal | ${testcase.tags} | ${expected} 35 | 36 | *** Keywords *** 37 | | Verify test case ${test_num} has ${expected} rows 38 | | | [Documentation] 39 | | | ... | Fail if the given test doesn't have the correct number of rows 40 | | | 41 | | | ${testcase}= | Set variable | ${testcase_table.testcases[${test_num}]} 42 | | | ${actual}= | Get length | ${testcase.rows} 43 | | | Should be equal as numbers | ${actual} | ${expected} 44 | | | ... | Expected '${testcase.name}' to have ${expected} rows but it had ${actual} 45 | | | ... | values=False 46 | | | 47 | | | [Teardown] | Run keyword if | "${Keyword Status}" == "FAIL" 48 | | | ... | log list | ${testcase.rows} 49 | -------------------------------------------------------------------------------- /tests/acceptance/self-test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | Runs rflint against the rflint test suites and resource files 4 | | 5 | | Library | OperatingSystem 6 | | Library | Process 7 | | Resource | SharedKeywords.robot 8 | | Test Template | Run rflint and verify there are no errors or warnings 9 | 10 | *** Test Cases *** 11 | # file to run rflint against | # expected return code 12 | | tests/unit/robotfile.robot | 0 13 | | tests/unit/testcase.robot | 0 14 | | tests/unit/keyword.robot | 0 15 | | tests/unit/UnitTestResources.robot | 0 16 | | tests/acceptance/smoke.robot | 0 17 | | tests/acceptance/self-test.robot | 0 18 | | tests/acceptance/arguments.robot | 0 19 | | tests/acceptance/rules/InvalidTable.robot | 0 20 | | tests/acceptance/rules/DuplicateKeywordNames.robot | 0 21 | | tests/acceptance/rules/PeriodInSuiteName.robot | 0 22 | | tests/acceptance/rules/PeriodInTestName.robot | 0 23 | | tests/acceptance/rules/TooManyTestCases.robot | 0 24 | | tests/acceptance/rules/TooManyTestSteps.robot | 0 25 | | tests/acceptance/rules/LineTooLong.robot | 0 26 | | tests/acceptance/rules/FileTooLong.robot | 0 27 | | tests/acceptance/rules/TrailingBlankLines.robot | 0 28 | 29 | *** Keywords *** 30 | | Run rflint and verify there are no errors or warnings 31 | | | [Arguments] | ${expected_rc} 32 | | | [Documentation] 33 | | | ... | Run rflint against the rflint tests 34 | | | 35 | | | ... | Note: it is assumed that the test name is the path of the file to test 36 | | | 37 | | | Run rf-lint with the following options: 38 | | | ... | --format | {severity}: {linenumber}, {char}: {message} ({rulename}) 39 | | | ... | --configure | TooFewTestSteps:1 40 | | | # because the test cases reference filenames, they all have 41 | | | # periods in their name... 42 | | | ... | --ignore | PeriodInTestName 43 | | | ... | ${test name} 44 | | | 45 | | | @{messages}= | Split to lines | ${result.stdout} 46 | | | ${warnings}= | Get match count | ${messages} | regexp=^W: 47 | | | ${errors}= | Get match count | ${messages} | regexp=^E: 48 | | | 49 | | | Run keyword if | "${result.rc}" != "${expected_rc}" or ${warnings} != 0 or ${errors} != 0 50 | | | ... | Fail | unexpectected errors or warnings: \n${result.stdout}\n${result.stderr} 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Welcome to Robot Framework Lint 2 | =============================== 3 | 4 | Static analysis for robot framework plain text files. 5 | 6 | This is a static analysis tool for robot framework plain text files. 7 | 8 | Installation Instructions 9 | ------------------------- 10 | 11 | The preferred method of installation is to use pip: 12 | 13 | $ pip install --upgrade robotframework-lint 14 | 15 | This will install a package named "rflint", and an executable named "rflint" 16 | 17 | Running the linter 18 | ------------------ 19 | 20 | To run, use the command "rflint", or use the `-m` option to python to 21 | run the rflint module. Add one or more filenames as arguments, and 22 | those files will be checked for rule violations. 23 | 24 | Custom rules 25 | ------------ 26 | 27 | Rules are simple python classes. For more information about how to 28 | write rules, see the 29 | [robotframework-lint wiki](https://github.com/boakley/robotframework-lint/wiki) 30 | 31 | Argument files 32 | -------------- 33 | 34 | rflint supports argument files much in the same way as robot framework. You can 35 | put arguments one per line in a file, and reference that file with the option 36 | `-A` or `--argument-file`. 37 | 38 | Argument files are a convenient way to create a set of rules and rule configurations 39 | that you want to apply to your files. 40 | 41 | Examples 42 | -------- 43 | 44 | $ rflint myTestSuite.robot 45 | 46 | To see a list of all of the built-in rules, run the following command 47 | 48 | $ rflint --list 49 | 50 | To see documentation, add the --verbose option: 51 | 52 | $ rflint --list --verbose 53 | 54 | Some rules are configurable. For example, to configure the "LineTooLong" 55 | rule to flag lines longer than 80 characters (the default is 100), you 56 | can change the default value with the configure option: 57 | 58 | $ rflint --configure LineTooLong:80 myTestSuite.robot 59 | 60 | You can disable any rule, or configure it to be a warning or error 61 | with the options --warning, --error and --ignore. For example, to 62 | ignore the LineTooLong rule you can do this: 63 | 64 | $ rflint --ignore LineTooLong myTestSuite.robot 65 | 66 | To see a list of all command line options, use the `--help` option: 67 | 68 | $ python -m rflint --help 69 | 70 | Example output: 71 | 72 | $ python -m rflint myTestSuite.robot 73 | + myTestSuite.robot 74 | W: 2, 0: No suite documentation (RequireSuiteDocumentation) 75 | E: 15, 0: No keyword documentation (RequireKeywordDocumentation) 76 | 77 | This show a warning on line two, character 0, where there should be suite 78 | documentation but isn't. It also shows an error on line 15, character 0, 79 | where there should be keyword documentation but there isn't. 80 | 81 | Acknowledgements 82 | ================ 83 | 84 | A huge thank-you to Echo Global Logistics (http://www.echo.com) for 85 | supporting the development of this package. 86 | -------------------------------------------------------------------------------- /rflint/rules/otherRules.py: -------------------------------------------------------------------------------- 1 | from rflint.common import TestRule, KeywordRule, GeneralRule, ERROR, WARNING 2 | 3 | import re 4 | 5 | 6 | class LineTooLong(GeneralRule): 7 | '''Check that a line is not too long (configurable; default=100)''' 8 | 9 | severity = WARNING 10 | maxchars = 100 11 | 12 | def configure(self, maxchars): 13 | self.maxchars = int(maxchars) 14 | 15 | def apply(self, robot_file): 16 | for linenumber, line in enumerate(robot_file.raw_text.splitlines()): 17 | if len(line) > self.maxchars: 18 | message = "Line is too long (exceeds %s characters)" % self.maxchars 19 | self.report(robot_file, message, linenumber+1, self.maxchars) 20 | 21 | class TrailingBlankLines(GeneralRule): 22 | '''Check for multiple blank lines at the end of a file 23 | 24 | This is a configurable. The default value is 2. 25 | ''' 26 | 27 | severity = WARNING 28 | max_allowed = 2 29 | 30 | def configure(self, max_allowed): 31 | self.max_allowed=int(max_allowed) 32 | 33 | def apply(self, robot_file): 34 | # I realize I'm making two full passes over the data, but 35 | # python is plenty fast enough. Even processing a file with 36 | # over six thousand lines, this takes a couple of 37 | # milliseconds. Plenty fast enough for the intended use case, 38 | # since most files should be about two orders of magnitude 39 | # smaller than that. 40 | 41 | match=re.search(r'(\s*)$', robot_file.raw_text) 42 | if match: 43 | count = len(re.findall(r'\n', match.group(0))) 44 | if count > self.max_allowed: 45 | numlines = len(robot_file.raw_text.splitlines()) 46 | message = "Too many trailing blank lines" 47 | linenumber = numlines-count 48 | self.report(robot_file, message, linenumber+self.max_allowed, 0) 49 | 50 | 51 | class TrailingWhitespace(GeneralRule): 52 | severity = WARNING 53 | 54 | def apply(self, robot_file): 55 | for linenumber, line in enumerate(robot_file.raw_text.splitlines()): 56 | if len(line) != len(line.rstrip()): 57 | message = "Line has trailing whitespace" 58 | self.report(robot_file, message, linenumber+1) 59 | 60 | 61 | class FileTooLong(GeneralRule): 62 | '''Verify the file has fewer lines than a given threshold. 63 | 64 | You can configure the maximum number of lines. The default is 300. 65 | ''' 66 | 67 | severity = WARNING 68 | max_allowed = 300 69 | 70 | def configure(self, max_allowed): 71 | self.max_allowed = int(max_allowed) 72 | 73 | def apply(self, robot_file): 74 | lines = robot_file.raw_text.splitlines() 75 | if len(lines) > self.max_allowed: 76 | message = "File has too many lines (%s)" % len(lines) 77 | linenumber = self.max_allowed+1 78 | self.report(robot_file, message, linenumber, 0) 79 | -------------------------------------------------------------------------------- /tests/acceptance/rules/TooManyTestCases.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the testcase rule 'TooManyTestCases' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Test suite WITH too many test cases 11 | | | [Documentation] 12 | | | ... | Verify that a test suite with too many test cases triggers the rule. 13 | | | 14 | | | [Setup] | Run rf-lint with the following options: 15 | | | ... | --no-filename 16 | | | ... | --ignore | all 17 | | | ... | --error | TooManyTestCases 18 | | | ... | test_data/acceptance/rules/TooManyTestCases.robot 19 | | | 20 | | | rflint return code should be | 1 21 | | | rflint should report 1 errors 22 | | | rflint should report 0 warnings 23 | 24 | | Verify correct linenumber is displayed 25 | | | [Documentation] 26 | | | ... | Verify that the reported line number points to the Nth test 27 | | | ... | 28 | | | ... | ie: if the max is 10, the error should point to the start 29 | | | ... | of the 11th test case. 30 | | | 31 | | | [Setup] | Run rf-lint with the following options: 32 | | | ... | --no-filename 33 | | | ... | --ignore | all 34 | | | ... | --error | TooManyTestCases 35 | | | ... | test_data/acceptance/rules/TooManyTestCases.robot 36 | | | 37 | | | Output should contain 38 | | | ... | E: 46, 0: Too many test cases (11 > 10) in test suite (TooManyTestCases) 39 | 40 | | | rflint should report 1 errors 41 | | | rflint should report 0 warnings 42 | 43 | | Testcase WITHOUT too many test cases 44 | | | [Documentation] 45 | | | ... | Verify that a test suite without too many test cases does NOT trigger the rule. 46 | | | 47 | | | [Setup] | Run rf-lint with the following options: 48 | | | ... | --no-filename 49 | | | ... | --ignore | all 50 | | | ... | --error | TooManyTestCases 51 | | | ... | ${SUITE SOURCE} | use this file as input 52 | | | 53 | | | rflint return code should be | 0 54 | | | rflint should report 0 errors 55 | | | rflint should report 0 warnings 56 | 57 | | Test suite WITH too many test cases after configuration 58 | | | [Documentation] 59 | | | ... | Verify that a test suite with too many test cases triggers the rule. 60 | | | 61 | | | [Setup] | Run rf-lint with the following options: 62 | | | ... | --no-filename 63 | | | ... | --ignore | all 64 | | | ... | --error | TooManyTestCases 65 | | | ... | --configure | TooManyTestCases:1 66 | | | ... | test_data/acceptance/rules/TooManyTestCases.robot 67 | | | 68 | | | rflint return code should be | 1 69 | | | rflint should report 1 errors 70 | | | rflint should report 0 warnings 71 | 72 | | Testcase WITHOUT too many test cases after configuration 73 | | | [Documentation] 74 | | | ... | Verify that a test suite without too many test cases does NOT trigger the rule. 75 | | | 76 | | | [Setup] | Run rf-lint with the following options: 77 | | | ... | --no-filename 78 | | | ... | --ignore | all 79 | | | ... | --error | TooManyTestCases 80 | | | ... | --configure | TooManyTestCases:20 81 | | | ... | test_data/acceptance/rules/TooManyTestCases.robot 82 | | | 83 | | | rflint return code should be | 0 84 | | | rflint should report 0 errors 85 | | | rflint should report 0 warnings 86 | -------------------------------------------------------------------------------- /rflint/rules/duplicates.py: -------------------------------------------------------------------------------- 1 | from rflint.common import SuiteRule, ResourceRule, ERROR, normalize_name 2 | 3 | def check_duplicates(report_duplicate, table, 4 | permitted_dups=None, normalize_itemname=normalize_name): 5 | # `table` is a SettingsTable or a VariableTable; either contains rows, 6 | # but only VariableTable also contains statements. 7 | seen_rows = {} 8 | for row in table.rows: 9 | item = normalize_itemname(row[0]) 10 | 11 | # skip empty lines, comments and continuation lines 12 | if item == "": 13 | continue 14 | if item.startswith("#"): 15 | continue 16 | if item.startswith("..."): 17 | continue 18 | 19 | # some tables allow duplicates 20 | if permitted_dups and item in permitted_dups: 21 | continue 22 | 23 | if item in seen_rows: 24 | prev_row = seen_rows[item] 25 | report_duplicate(row, prev_row) 26 | else: 27 | seen_rows[item] = row 28 | 29 | 30 | class DuplicateSettingsCommon(object): 31 | '''Verify that settings are not repeated in a Settings table 32 | 33 | This has been made an error in Robot3.0 34 | https://github.com/robotframework/robotframework/issues/2204''' 35 | severity = ERROR 36 | 37 | def apply(self, suite): 38 | def report_duplicate_setting(setting, prev_setting): 39 | self.report(suite, 40 | "Setting '%s' used multiple times (previously used line %d)" % \ 41 | (setting[0], prev_setting.linenumber), setting.linenumber) 42 | 43 | for table in suite.tables: 44 | if table.name == "Settings": 45 | check_duplicates(report_duplicate_setting, table, 46 | permitted_dups=["library", "resource", "variables"]) 47 | 48 | class DuplicateSettingsInSuite(DuplicateSettingsCommon, SuiteRule): 49 | pass 50 | 51 | class DuplicateSettingsInResource(DuplicateSettingsCommon, ResourceRule): 52 | pass 53 | 54 | 55 | def strip_variable_name(varname): 56 | return varname.lstrip("${").rstrip("}= ") 57 | 58 | def normalize_variable_name(varname): 59 | return normalize_name(strip_variable_name(varname)) 60 | 61 | class DuplicateVariablesCommon(object): 62 | '''Verify that variables are not defined twice in the same table 63 | 64 | This is not an error, but leads to surprising result (first definition 65 | wins, later is ignored).''' 66 | def apply(self, suite): 67 | def report_duplicate_variable(variable, prev_variable): 68 | self.report(suite, 69 | "Variable '%s' defined twice, previous definition line %d" % \ 70 | (strip_variable_name(variable[0]), prev_variable.linenumber), 71 | variable.linenumber) 72 | 73 | for table in suite.tables: 74 | if table.name == "Variables": 75 | check_duplicates(report_duplicate_variable, table, 76 | normalize_itemname=normalize_variable_name) 77 | 78 | class DuplicateVariablesInSuite(DuplicateVariablesCommon, SuiteRule): 79 | pass 80 | 81 | class DuplicateVariablesInResource(DuplicateVariablesCommon, ResourceRule): 82 | pass 83 | -------------------------------------------------------------------------------- /tests/acceptance/rules/TooManyTestSteps.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the testcase rule 'TooManyTestSteps' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Testcase WITHOUT too many test steps 11 | | | [Documentation] 12 | | | ... | Verify that a testcase without too many test steps triggers the rule. 13 | | | 14 | | | [Setup] | Run rf-lint with the following options: 15 | | | ... | --no-filename 16 | | | ... | --ignore | all 17 | | | ... | --error | TooManyTestSteps 18 | | | ... | ${SUITE SOURCE} | use this file as input 19 | | | 20 | | | No operation 21 | | | No operation 22 | | | No operation 23 | | | No operation 24 | | | No operation 25 | | | No operation 26 | | | No operation 27 | | | rflint return code should be | 0 28 | | | rflint should report 0 errors 29 | | | rflint should report 0 warnings 30 | 31 | | Testcase WITH too many test steps 32 | | | [Documentation] 33 | | | ... | Verify that a testcase with too many test steps triggers the rule. 34 | | | 35 | | | [Setup] | Run rf-lint with the following options: 36 | | | ... | --no-filename 37 | | | ... | --ignore | all 38 | | | ... | --error | TooManyTestSteps 39 | | | ... | test_data/acceptance/rules/TooManyTestSteps.robot 40 | | | 41 | | | rflint return code should be | 1 42 | | | rflint should report 1 errors 43 | | | rflint should report 0 warnings 44 | 45 | | Verify correct linenumber is displayed 46 | | | [Documentation] 47 | | | ... | Verify that the reported line number points to the Nth step 48 | | | ... | 49 | | | ... | ie: if the max is 10, the error should point to the start 50 | | | ... | of the 11th step. 51 | | | 52 | | | [Setup] | Run rf-lint with the following options: 53 | | | ... | --no-filename 54 | | | ... | --ignore | all 55 | | | ... | --error | TooManyTestSteps 56 | | | ... | test_data/acceptance/rules/TooManyTestSteps.robot 57 | | | 58 | | | Output should contain 59 | | | ... | E: 19, 0: Too many steps (15) in test case (TooManyTestSteps) 60 | 61 | | Testcase WITHOUT too many test steps after configuration 62 | | | [Documentation] 63 | | | ... | Verify that a testcase with too many test steps triggers the rule. 64 | | | 65 | | | [Setup] | Run rf-lint with the following options: 66 | | | ... | --no-filename 67 | | | ... | --ignore | all 68 | | | ... | --error | TooManyTestSteps 69 | | | ... | --configure | TooManyTestSteps:20 70 | | | ... | test_data/acceptance/rules/TooManyTestSteps.robot 71 | | | 72 | | | rflint return code should be | 0 73 | | | rflint should report 0 errors 74 | | | rflint should report 0 warnings 75 | 76 | | Testcase WITH too many test steps after configuration 77 | | | [Documentation] 78 | | | ... | Verify that a testcase with too many test steps triggers the rule. 79 | | | 80 | | | [Setup] | Run rf-lint with the following options: 81 | | | ... | --no-filename 82 | | | ... | --ignore | all 83 | | | ... | --error | TooManyTestSteps 84 | | | ... | --configure | TooManyTestSteps:1 85 | | | ... | test_data/acceptance/rules/TooManyTestSteps.robot 86 | | | 87 | | | rflint return code should be | 1 88 | | | rflint should report 1 errors 89 | | | rflint should report 0 warnings 90 | -------------------------------------------------------------------------------- /tests/acceptance/arguments.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | This suite includes tests for the command line arguments 4 | | # 5 | | Library | OperatingSystem 6 | | Library | Process 7 | | Library | SharedKeywords.py 8 | | Resource | SharedKeywords.robot 9 | | Force Tags | smoke 10 | | # 11 | | Test Teardown 12 | | # provide some debugging information if things go bad 13 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 14 | | ... | log | ${result.stdout}\n${result.stderr} 15 | 16 | *** Test Cases *** 17 | | No filenames 18 | | | [Documentation] 19 | | | ... | Verify that the --no-filenames supresses filenames 20 | | | 21 | | | Run rf-lint with the following options: 22 | | | ... | --ignore | all 23 | | | ... | --warning | RequireSuiteDocumentation 24 | | | ... | --no-filenames 25 | | | ... | test_data/acceptance/nodoc.robot 26 | | | 27 | | | Stderr should be | ${EMPTY} 28 | | | Stdout should be 29 | | | ... | W: 1, 0: No suite documentation (RequireSuiteDocumentation) 30 | 31 | | Default output includes filenames 32 | | | [Documentation] 33 | | | ... | Verify that the --no-filenames supresses filenames 34 | | | 35 | | | Run rf-lint with the following options: 36 | | | ... | --ignore | all 37 | | | ... | --warning | RequireSuiteDocumentation 38 | | | ... | test_data/acceptance/nodoc.robot 39 | | | 40 | | | Stderr should be | ${EMPTY} 41 | | | Stdout should be 42 | | | ... | + test_data/acceptance/nodoc.robot 43 | | | ... | W: 1, 0: No suite documentation (RequireSuiteDocumentation) 44 | 45 | | Same file twice should show filename twice 46 | | | [Documentation] 47 | | | ... | Verify that we print each filename that is processed 48 | | | ... | 49 | | | ... | If a filename is given on the command line twice, that 50 | | | ... | filename should be printed twice. 51 | | | 52 | | | Run rf-lint with the following options: 53 | | | ... | --ignore | all 54 | | | ... | --warning | RequireSuiteDocumentation 55 | | | ... | test_data/acceptance/nodoc.robot 56 | | | ... | test_data/acceptance/nodoc.robot 57 | | | 58 | | | Stderr should be | ${EMPTY} 59 | | | Stdout should be 60 | | | ... | + test_data/acceptance/nodoc.robot 61 | | | ... | W: 1, 0: No suite documentation (RequireSuiteDocumentation) 62 | | | ... | + test_data/acceptance/nodoc.robot 63 | | | ... | W: 1, 0: No suite documentation (RequireSuiteDocumentation) 64 | 65 | | Rulefile option 66 | | | [Documentation] 67 | | | ... | Verify that rules in a rulefile are listed 68 | | | 69 | | | Run rf-lint with the following options: 70 | | | ... | --rulefile | test_data/acceptance/customRules.py 71 | | | ... | --list 72 | | | Stderr should be | ${EMPTY} 73 | | | Output should contain 74 | | | ... | W CustomTestRule 75 | | | ... | W CustomSuiteRule 76 | | | ... | W CustomGeneralRule 77 | | | ... | W CustomKeywordRule 78 | 79 | | The --describe option with one named rule 80 | | | [Documentation] 81 | | | ... | Verify that --describe works 82 | | | Run rf-lint with the following options: 83 | | | ... | --describe | RequireKeywordDocumentation 84 | | | rflint return code should be | 0 85 | | | Stderr should be | ${EMPTY} 86 | | | Stdout should be 87 | | | ... | RequireKeywordDocumentation 88 | | | ... | ${SPACE*4}Verify that a keyword has documentation 89 | 90 | | The --describe option with invalid rule name 91 | | | [Documentation] 92 | | | ... | Verify that rflint --describe fails if given unknown rule name 93 | | | Run rf-lint with the following options: 94 | | | ... | --describe | BogusRule 95 | | | rflint return code should be | 1 96 | | | Stderr should be | 97 | | | ... | unknown rule: 'BogusRule' 98 | | | Stdout should be | ${EMPTY} 99 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | ## 1.0 - 2019-07-24 3 | 4 | ### Issues closed 5 | - Issue #37 - trailing whitespace on line 6 | - Issue #54 - Fix the recursive option 7 | Thanks to Bassam Khouri for the fix 8 | - Issue #62 - Comments included in keyword settings 9 | - Issue #63 - Multiple instances of RfLint will result in duplicate rules 10 | 11 | ### New Rules 12 | - TrailingWhitespace 13 | 14 | ### Other Changes 15 | - fixed some unicode issues 16 | - tested with python 3.7.2 17 | 18 | ## 0.7.0 - 2016-03-07 19 | - fix for issue #30 - preserve table headings 20 | 21 | ## 0.6.1 - 2015-07-22 22 | 23 | - added `walk` method to RobotFile class, that somehow got 24 | left out in the 0.6 version. 25 | 26 | ## 0.6 - 2015-07-21 27 | 28 | ### New rules 29 | - TooFewTestSteps 30 | - TooFewKeywordSteps 31 | 32 | ### Issues closed 33 | - Issue #24 - InvalidTable isn't catching everything 34 | - Issue #25 - Rules to detect empty tests and keywords 35 | - Issue #27 - add --describe option 36 | - Issue #28 - --rulefile isn't working 37 | - Issue #30 - When ResourceRule class has configure method, rflint says the rule is unknown. 38 | - Issue #31 - A GeneralRule class rule is not passed an object with a type attribute 39 | 40 | ### Other changes 41 | - small improvements to the custom parser 42 | 43 | ## 0.5 - 2015-01-26 44 | 45 | ### New rules 46 | - Configurable rules 47 | - New General rules: 48 | - LineTooLong 49 | - FileTooLong 50 | - TrailingBlankLines 51 | - New testcase rules 52 | - TooManySteps (provided by guykisel) 53 | - TooManyTestCases (provided by guykisel) 54 | 55 | ### Issues closed 56 | - issue #22 - FileTooLong rule 57 | - issue #19 - rules should accept arguments 58 | - issue #5 - "bare" comments are not parsed properly 59 | 60 | ### Other changes 61 | - Rflint now distinguishes between resource files and test suites 62 | by checking whether the file has a testcase table or not 63 | - General rules now have access to the raw text of a file, so 64 | they can do their own parsing if they want (issue #5) 65 | 66 | 67 | ## 0.4 - 2014-12-22 68 | 69 | ### New rules 70 | 71 | - new Suite rules: 72 | - PeriodInSuiteName 73 | - InvalidTable 74 | 75 | - new Testcase rules: 76 | - PeriodInTestName 77 | 78 | ### Issues closed: 79 | - issue #1 - Add -A/--argumentfile 80 | - issue #2 - Need verbose option for --list 81 | - issue #3 - --list output includes unnecessary quotes 82 | - issue #4 - Add ability to process directories 83 | - issue #7 - Add "rflint" script for easier use 84 | - issue #13 - non-breaking spaces in a test file 85 | - issue #15 - only list files that have errors/warnings 86 | - issue #20 - Add --rulefile option for loading rules by filename 87 | - issue #21 - "file not found" should be printed for bad filenames 88 | 89 | ### Other changes 90 | - internally, a parsed file is now either an instance of rflint.SuiteFile 91 | or rflint.ResourceFile, depending on whether the file has a testcase 92 | table in it. Prior to this change, the object was always of class RobotFile. 93 | Both of these classes are a subclass of rflint.RobotFile, so any old 94 | code that depends on that should continue to work. 95 | 96 | - All internal rule classes now have a "doc" attribute for 97 | returning the docstring without leading whitespace on each line. 98 | 99 | ## 0.3 - 2014-12-02 100 | ### Added 101 | - new command line options: 102 | - --version 103 | - added some acceptance tests 104 | 105 | ### Fixed 106 | - RequireTestDocumentation no longer will generate a message if the 107 | suite is templated, since documentation in templated tests is cumbersome 108 | -------------------------------------------------------------------------------- /rflint/common.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | ERROR = "E" 4 | WARNING = "W" 5 | IGNORE = "I" 6 | 7 | 8 | def normalize_name(string): 9 | '''convert to lowercase, remove spaces and underscores''' 10 | return string.replace(" ", "").replace("_", "").lower() 11 | 12 | 13 | class Rule(object): 14 | # default severity; subclasses may override 15 | severity = WARNING 16 | output_format = "{severity}: {linenumber}, {char}: {message} ({rulename})" 17 | 18 | def __init__(self, controller, severity=None): 19 | self.controller = controller 20 | if severity is not None: 21 | self.severity = severity 22 | 23 | def configure(self, *args, **kwargs): 24 | # subclasses can override this if they want to be configurable. 25 | raise Exception("rule '%s' is not configurable" % self.__class__.__name__) 26 | 27 | @property 28 | def name(self): 29 | return self.__class__.__name__ 30 | 31 | def report(self, obj, message, linenum, char_offset=0): 32 | """Report an error or warning""" 33 | self.controller.report(linenumber=linenum, filename=obj.path, 34 | severity=self.severity, message=message, 35 | rulename = self.__class__.__name__, 36 | char=char_offset) 37 | 38 | @property 39 | def doc(self): 40 | '''Algorithm from https://www.python.org/dev/peps/pep-0257/''' 41 | if not self.__doc__: 42 | return "" 43 | 44 | lines = self.__doc__.expandtabs().splitlines() 45 | 46 | # Determine minimum indentation (first line doesn't count): 47 | indent = sys.maxsize 48 | for line in lines[1:]: 49 | stripped = line.lstrip() 50 | if stripped: 51 | indent = min(indent, len(line) - len(stripped)) 52 | 53 | # Remove indentation (first line is special): 54 | trimmed = [lines[0].strip()] 55 | if indent < sys.maxsize: 56 | for line in lines[1:]: 57 | trimmed.append(line[indent:].rstrip()) 58 | 59 | # Strip off trailing and leading blank lines: 60 | while trimmed and not trimmed[-1]: 61 | trimmed.pop() 62 | while trimmed and not trimmed[0]: 63 | trimmed.pop(0) 64 | 65 | # Return a single string: 66 | return '\n'.join(trimmed) 67 | 68 | def __repr__(self): 69 | return "%s %s" % (self.severity, self.__class__.__name__) 70 | 71 | class TestRule(Rule): 72 | """Rule that runs against test cases. 73 | 74 | The object that is passed in will be of type rflint.parser.Testcase 75 | """ 76 | pass 77 | 78 | class ResourceRule(Rule): 79 | """Rule that runs against a resource file 80 | 81 | The object that is passed in will be of type rflint.parser.ResourceFile 82 | """ 83 | 84 | class SuiteRule(Rule): 85 | """Rule that runs against test cases. 86 | 87 | The object that is passed in will be of type rflint.parser.SuiteFile 88 | """ 89 | pass 90 | 91 | class KeywordRule(Rule): 92 | """Rule that runs against keywords 93 | 94 | The object that is passed in will be of type rflint.parser.Keyword 95 | """ 96 | pass 97 | 98 | class GeneralRule(Rule): 99 | """Rule that requires a suite or resource file, but may apply to child objects 100 | 101 | This rule is identical to a SuiteRule, but exists in case you want 102 | to write a rule that accepts a suite but doesn't necessarily apply 103 | to the suite (ie: you may iterate over tests, or keywords, or some 104 | other child object) 105 | """ 106 | pass 107 | 108 | -------------------------------------------------------------------------------- /tests/unit/robotfile.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Unit tests for the RobotFile object 3 | | Library | Collections 4 | | Resource | UnitTestResources.robot 5 | | Suite Setup | Parse a robot file and save as a suite variable 6 | 7 | *** Test Cases *** 8 | | Parsed object has correct .name attribute 9 | | | [Documentation] | Verify that the parsed data has a name attribute 10 | | | Should be equal | ${rf.name} | pipes 11 | 12 | | Parsed object has correct .path attribute 13 | | | [Documentation] | Verify that the path attribute is correct 14 | | | ${expected}= | Evaluate | os.path.abspath("${test_data}") | os 15 | | | ${actual}= | Set variable | ${rf.path} 16 | | | Should be equal as strings | ${actual} | ${expected} 17 | | | ... | Expected .path to be '${expected}' but it was '${actual}' 18 | | | ... | values=False 19 | 20 | | All tables are correctly identified 21 | | | [Documentation] | Verify that the parser found all of the tables, and in the right order 22 | | | # N.B. using [template] allows all keywords to run even if some fail... 23 | | | [Template] | Run keyword 24 | | | Length should be | ${rf.tables} | 5 25 | | | ... | Expected to find 5 tables but did not (see test teardown for more information) 26 | | | Should be equal as strings 27 | | | ... | ${rf.tables[0].__class__} | 28 | | | Should be equal as strings 29 | | | ... | ${rf.tables[1].__class__} | 30 | | | Should be equal as strings 31 | | | ... | ${rf.tables[2].__class__} | 32 | | | Should be equal as strings 33 | | | ... | ${rf.tables[3].__class__} | 34 | | | Should be equal as strings 35 | | | ... | ${rf.tables[4].__class__} | 36 | | | 37 | | | [Teardown] | Run keyword if | "${Test Status}" == "FAIL" 38 | | | ... | Log list | ${rf.tables} 39 | 40 | | Tables have expected number of rows 41 | | | [Documentation] 42 | | | ... | Verify that each table has the expected number of rows 43 | | | ... | 44 | | | ... | Note: the parser doesn't keep track of rows in a testcase or 45 | | | ... | keyword table since that information is tracked in each testcase 46 | | | ... | and keyword object. Therefore this test only covers the settings, 47 | | | ... | variables and bogus tables 48 | | | ... | 49 | | | ... | Also, the parser treats blank lines as rows, so the number of 50 | | | ... | rows needs to account for trailing blank lines. 51 | | | 52 | | | [Template] | Run keyword 53 | | | Verify table 0 has 8 rows 54 | | | Verify table 1 has 5 rows 55 | | | Verify table 3 has 2 rows 56 | 57 | | Tables have header information 58 | | | [Documentation] 59 | | | ... | Verify that table objects have header information 60 | | | 61 | | | Should be equal as strings | ${rf.tables[0].header} | *** Settings *** 62 | | | Should be equal as strings | ${rf.tables[1].header} | ** Variables 63 | | | Should be equal as strings | ${rf.tables[2].header} | * Test Cases * 64 | | | Should be equal as strings | ${rf.tables[3].header} | *** Bogus Table 65 | | | Should be equal as strings | ${rf.tables[4].header} | * Keywords *** 66 | 67 | 68 | *** Keywords *** 69 | | Verify table ${table_num} has ${expected} rows 70 | | | [Documentation] 71 | | | ... | Fail if the given table doesn't have the correct number of rows 72 | | | 73 | | | ${table}= | Set variable | ${rf.tables[${table_num}]} 74 | | | ${actual}= | Get length | ${table.rows} 75 | | | Should be equal as numbers | ${actual} | ${expected} 76 | | | ... | Expected '${table.name}' to have ${expected} rows but it had ${actual} 77 | | | ... | values=False 78 | | | 79 | | | [Teardown] | Run keyword if | "${Keyword Status}" == "FAIL" 80 | | | ... | log list | ${table.rows} 81 | 82 | -------------------------------------------------------------------------------- /rflint/rules/testcaseRules.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2014 Bryan Oakley 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | ''' 16 | 17 | from rflint.common import TestRule, ERROR, WARNING 18 | from rflint.parser import SettingTable 19 | 20 | 21 | class PeriodInTestName(TestRule): 22 | '''Warn about periods in the testcase name 23 | 24 | Since robot uses "." as a path separator, using a "." in a testcase 25 | name can lead to ambiguity. 26 | ''' 27 | severity = WARNING 28 | 29 | def apply(self,testcase): 30 | if "." in testcase.name: 31 | self.report(testcase, "'.' in testcase name '%s'" % testcase.name, testcase.linenumber) 32 | 33 | class TagWithSpaces(TestRule): 34 | '''Flags tags that have spaces in the tag name''' 35 | severity=ERROR 36 | 37 | def apply(self, testcase): 38 | for tag in testcase.tags: 39 | if ((" " in tag) or ("\t" in tag)): 40 | self.report(testcase, "space not allowed in tag name: '%s'" % tag, testcase.linenumber) 41 | 42 | class RequireTestDocumentation(TestRule): 43 | '''Verify that a test suite has documentation 44 | 45 | This rule is not enforced for data driven tests ("Test Template" in Settings) 46 | ''' 47 | severity=ERROR 48 | 49 | def apply(self, testcase): 50 | if testcase.is_templated: 51 | return 52 | 53 | for setting in testcase.settings: 54 | if setting[1].lower() == "[documentation]" and len(setting) > 2: 55 | return 56 | 57 | # set the line number to the line immediately after the testcase name 58 | self.report(testcase, "No testcase documentation", testcase.linenumber+1) 59 | 60 | class TooFewTestSteps(TestRule): 61 | '''Tests should have at least a minimum number of steps 62 | 63 | This rule is configurable. The default number of required steps is 2. 64 | ''' 65 | 66 | min_required = 2 67 | 68 | def configure(self, min_required): 69 | self.min_required = int(min_required) 70 | 71 | def apply(self, testcase): 72 | if testcase.is_templated: 73 | return 74 | 75 | # ignore empty test steps 76 | steps = [step for step in testcase.steps if not (len(step) == 1 77 | and not step[0])] 78 | if len(steps) < self.min_required: 79 | msg = "Too few steps (%s) in test case" % len(steps) 80 | self.report(testcase, msg, testcase.linenumber) 81 | 82 | class TooManyTestSteps(TestRule): 83 | '''Workflow tests should have no more than ten steps. 84 | 85 | https://code.google.com/p/robotframework/wiki/HowToWriteGoodTestCases#Workflow_tests 86 | ''' 87 | 88 | severity=WARNING 89 | max_allowed = 10 90 | 91 | def configure(self, max_allowed): 92 | self.max_allowed = int(max_allowed) 93 | 94 | def apply(self, testcase): 95 | if testcase.is_templated: 96 | return 97 | 98 | # ignore empty test steps 99 | steps = [step for step in testcase.steps if not (len(step) == 1 100 | and not step[0])] 101 | if len(steps) > self.max_allowed: 102 | self.report(testcase, 103 | "Too many steps (%s) in test case" % len(steps), 104 | steps[self.max_allowed].startline) 105 | -------------------------------------------------------------------------------- /tests/acceptance/rules/InvalidTable.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the rule 'InvalidTable' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify all invalid table names in Robot 2 are detected 11 | | | [Documentation] 12 | | | ... | Verify that all invalid table names cause errors, 13 | | | ... | and all valid names do not. Note: the test data 14 | | | ... | is a collection of both valid and invalid names. 15 | | | ... | Robot 2 accepted a few synonyms that were deprecated 16 | | | ... | in Robot 3. 17 | | | 18 | | | [Setup] | Run rf-lint with the following options: 19 | | | ... | --no-filename 20 | | | ... | --ignore | all 21 | | | ... | --error | InvalidTable 22 | | | ... | --configure | InvalidTable:robot2 23 | | | ... | test_data/acceptance/rules/InvalidTable_Data.robot 24 | | | 25 | | | rflint return code should be | 6 26 | | | rflint should report 6 errors 27 | | | rflint should report 0 warnings 28 | 29 | | Verify that the proper error message is returned with Robot 2 option 30 | | | [Documentation] 31 | | | ... | Verify that InvalidTable returns the expected message 32 | | | ... | for every error 33 | | | ... | Robot 2 accepted a few synonyms that were deprecated 34 | | | ... | in Robot 3. 35 | | | 36 | | | [Setup] | Run rf-lint with the following options: 37 | | | ... | --no-filename 38 | | | ... | --ignore | all 39 | | | ... | --error | InvalidTable 40 | | | ... | --configure | InvalidTable:robot2 41 | | | ... | test_data/acceptance/rules/InvalidTable_Data.robot 42 | | | 43 | | | Output should contain 44 | | | ... | E: 6, 0: Unknown table name '' (InvalidTable) 45 | | | ... | E: 7, 0: Unknown table name '' (InvalidTable) 46 | | | ... | E: 8, 0: Unknown table name '' (InvalidTable) 47 | | | ... | E: 9, 0: Unknown table name 'Testcase' (InvalidTable) 48 | | | ... | E: 10, 0: Unknown table name 'Key word' (InvalidTable) 49 | | | ... | E: 37, 0: Unknown table name 'bogus' (InvalidTable) 50 | 51 | | Verify all invalid table names in Robot 3 are detected 52 | | | [Documentation] 53 | | | ... | Verify that all invalid table names cause errors, 54 | | | ... | and all valid names do not. Note: the test data 55 | | | ... | is a collection of both valid and invalid names. 56 | | | ... | Robot 3 obsoleted some synonyms that were valid with 57 | | | ... | Robot 2. 58 | | | 59 | | | [Setup] | Run rf-lint with the following options: 60 | | | ... | --no-filename 61 | | | ... | --ignore | all 62 | | | ... | --error | InvalidTable 63 | | | ... | --configure | InvalidTable:robot3 64 | | | ... | test_data/acceptance/rules/InvalidTable_Data.robot 65 | | | 66 | | | rflint return code should be | 10 67 | | | rflint should report 10 errors 68 | | | rflint should report 0 warnings 69 | 70 | | Verify that the proper error message is returned 71 | | | [Documentation] 72 | | | ... | Verify that InvalidTable returns the expected message 73 | | | ... | for every error, using default option (Robot 3 syntax). 74 | | | ... | Robot 3 obsoleted some synonyms that were valid with 75 | | | ... | Robot 2. 76 | | | 77 | | | [Setup] | Run rf-lint with the following options: 78 | | | ... | --no-filename 79 | | | ... | --ignore | all 80 | | | ... | --error | InvalidTable 81 | | | ... | test_data/acceptance/rules/InvalidTable_Data.robot 82 | | | 83 | | | Output should contain 84 | | | ... | E: 6, 0: Unknown table name '' (InvalidTable) 85 | | | ... | E: 7, 0: Unknown table name '' (InvalidTable) 86 | | | ... | E: 8, 0: Unknown table name '' (InvalidTable) 87 | | | ... | E: 9, 0: Unknown table name 'Testcase' (InvalidTable) 88 | | | ... | E: 10, 0: Unknown table name 'Key word' (InvalidTable) 89 | | | ... | E: 24, 0: Unknown table name 'Metadata' (InvalidTable) 90 | | | ... | E: 25, 0: Unknown table name 'Cases' (InvalidTable) 91 | | | ... | E: 32, 0: Unknown table name 'User Keyword' (InvalidTable) 92 | | | ... | E: 33, 0: Unknown table name 'User Keywords' (InvalidTable) 93 | | | ... | E: 37, 0: Unknown table name 'bogus' (InvalidTable) 94 | -------------------------------------------------------------------------------- /tests/acceptance/rules/InvalidTableInResource.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Tests for the rule 'InvalidTableInResource' 3 | | Resource | ../SharedKeywords.robot 4 | | 5 | | Test Teardown 6 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 7 | | ... | log | ${result.stdout}\n${result.stderr} 8 | 9 | *** Test Cases *** 10 | | Verify all invalid table names in Robot 2 are detected 11 | | | [Documentation] 12 | | | ... | Verify that all invalid table names in a resource 13 | | | ... | file cause errors, and all valid names do not. 14 | | | ... | Robot 2 accepted a few synonyms that were deprecated 15 | | | ... | in Robot 3. 16 | | | ... | Note: the test data is a collection of both valid 17 | | | ... | and invalid names. 18 | | | 19 | | | [Setup] | Run rf-lint with the following options: 20 | | | ... | --no-filename 21 | | | ... | --ignore | all 22 | | | ... | --error | InvalidTableInResource 23 | | | ... | --configure | InvalidTableInResource:robot2 24 | | | ... | test_data/acceptance/rules/InvalidTableInResource_Data.robot 25 | | | 26 | | | rflint return code should be | 5 27 | | | rflint should report 5 errors 28 | | | rflint should report 0 warnings 29 | 30 | | Verify that the proper error message is returned with Robot 2 option 31 | | | [Documentation] 32 | | | ... | Verify that InvalidTableInResource returns the 33 | | | ... | expected message for every error 34 | | | ... | Robot 2 accepted a few synonyms that were deprecated 35 | | | ... | in Robot 3. 36 | | | 37 | | | [Setup] | Run rf-lint with the following options: 38 | | | ... | --no-filename 39 | | | ... | --ignore | all 40 | | | ... | --error | InvalidTableInResource 41 | | | ... | --configure | InvalidTableInResource:robot2 42 | | | ... | test_data/acceptance/rules/InvalidTableInResource_Data.robot 43 | | | 44 | | | Output should contain 45 | | | ... | E: 6, 0: Unknown table name '' (InvalidTableInResource) 46 | | | ... | E: 7, 0: Unknown table name '' (InvalidTableInResource) 47 | | | ... | E: 8, 0: Unknown table name '' (InvalidTableInResource) 48 | | | ... | E: 9, 0: Unknown table name 'Key word' (InvalidTableInResource) 49 | | | ... | E: 33, 0: Unknown table name 'bogus' (InvalidTableInResource) 50 | 51 | | Verify all invalid table names in Robot 3 are detected 52 | | | [Documentation] 53 | | | ... | Verify that all invalid table names in a resource 54 | | | ... | file cause errors, and all valid names do not. 55 | | | ... | Robot 3 obsoleted some synonyms that were valid with 56 | | | ... | Robot 2. 57 | | | ... | Note: the test data is a collection of both valid 58 | | | ... | and invalid names. 59 | | | 60 | | | [Setup] | Run rf-lint with the following options: 61 | | | ... | --no-filename 62 | | | ... | --ignore | all 63 | | | ... | --error | InvalidTableInResource 64 | | | ... | test_data/acceptance/rules/InvalidTableInResource_Data.robot 65 | | | 66 | | | rflint return code should be | 8 67 | | | rflint should report 8 errors 68 | | | rflint should report 0 warnings 69 | 70 | | Verify that the proper error message is returned 71 | | | [Documentation] 72 | | | ... | Verify that InvalidTableInResource returns the expected message 73 | | | ... | for every error, using default option (Robot 3 syntax). 74 | | | ... | Robot 2 accepted a few synonyms that were deprecated 75 | | | ... | in Robot 3. 76 | | | 77 | | | [Setup] | Run rf-lint with the following options: 78 | | | ... | --no-filename 79 | | | ... | --ignore | all 80 | | | ... | --error | InvalidTableInResource 81 | | | ... | test_data/acceptance/rules/InvalidTableInResource_Data.robot 82 | | | 83 | | | Output should contain 84 | | | ... | E: 6, 0: Unknown table name '' (InvalidTableInResource) 85 | | | ... | E: 7, 0: Unknown table name '' (InvalidTableInResource) 86 | | | ... | E: 8, 0: Unknown table name '' (InvalidTableInResource) 87 | | | ... | E: 9, 0: Unknown table name 'Key word' (InvalidTableInResource) 88 | | | ... | E: 23, 0: Unknown table name 'Metadata' (InvalidTableInResource) 89 | | | ... | E: 28, 0: Unknown table name 'User Keyword' (InvalidTableInResource) 90 | | | ... | E: 29, 0: Unknown table name 'User Keywords' (InvalidTableInResource) 91 | | | ... | E: 33, 0: Unknown table name 'bogus' (InvalidTableInResource) 92 | -------------------------------------------------------------------------------- /test_data/acceptance/rules/FileTooLong_Data.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | This file needs to be > 300 lines long, to test the 4 | | ... | default limit of the rule FileTooLong 5 | | ... | 6 | | ... | 7 | | ... | 8 | | ... | 9 | | ... | 10 | | ... | line 10 11 | | ... | 12 | | ... | 13 | | ... | 14 | | ... | 15 | | ... | 16 | | ... | 17 | | ... | 18 | | ... | 19 | | ... | 20 | | ... | line 20 21 | | ... | 22 | | ... | 23 | | ... | 24 | | ... | 25 | | ... | 26 | | ... | 27 | | ... | 28 | | ... | 29 | | ... | 30 | | ... | line 30 31 | | ... | 32 | | ... | 33 | | ... | 34 | | ... | 35 | | ... | 36 | | ... | 37 | | ... | 38 | | ... | 39 | | ... | 40 | | ... | line 40 41 | | ... | 42 | | ... | 43 | | ... | 44 | | ... | 45 | | ... | 46 | | ... | 47 | | ... | 48 | | ... | 49 | | ... | 50 | | ... | line 50 51 | | ... | 52 | | ... | 53 | | ... | 54 | | ... | 55 | | ... | 56 | | ... | 57 | | ... | 58 | | ... | 59 | | ... | 60 | | ... | line 60 61 | | ... | 62 | | ... | 63 | | ... | 64 | | ... | 65 | | ... | 66 | | ... | 67 | | ... | 68 | | ... | 69 | | ... | 70 | | ... | line 70 71 | | ... | 72 | | ... | 73 | | ... | 74 | | ... | 75 | | ... | 76 | | ... | 77 | | ... | 78 | | ... | 79 | | ... | 80 | | ... | line 80 81 | | ... | 82 | | ... | 83 | | ... | 84 | | ... | 85 | | ... | 86 | | ... | 87 | | ... | 88 | | ... | 89 | | ... | 90 | | ... | line 90 91 | | ... | 92 | | ... | 93 | | ... | 94 | | ... | 95 | | ... | 96 | | ... | 97 | | ... | 98 | | ... | 99 | | ... | 100 | | ... | line 100 101 | | ... | 102 | | ... | 103 | | ... | 104 | | ... | 105 | | ... | 106 | | ... | 107 | | ... | 108 | | ... | 109 | | ... | 110 | | ... | line 110 111 | | ... | 112 | | ... | 113 | | ... | 114 | | ... | 115 | | ... | 116 | | ... | 117 | | ... | 118 | | ... | 119 | | ... | 120 | | ... | line 120 121 | | ... | 122 | | ... | 123 | | ... | 124 | | ... | 125 | | ... | 126 | | ... | 127 | | ... | 128 | | ... | 129 | | ... | 130 | | ... | line 130 131 | | ... | 132 | | ... | 133 | | ... | 134 | | ... | 135 | | ... | 136 | | ... | 137 | | ... | 138 | | ... | 139 | | ... | 140 | | ... | line 140 141 | | ... | 142 | | ... | 143 | | ... | 144 | | ... | 145 | | ... | 146 | | ... | 147 | | ... | 148 | | ... | 149 | | ... | 150 | | ... | line 150 151 | | ... | 152 | | ... | 153 | | ... | 154 | | ... | 155 | | ... | 156 | | ... | 157 | | ... | 158 | | ... | 159 | | ... | 160 | | ... | line 160 161 | | ... | 162 | | ... | 163 | | ... | 164 | | ... | 165 | | ... | 166 | | ... | 167 | | ... | 168 | | ... | 169 | | ... | 170 | | ... | line 170 171 | | ... | 172 | | ... | 173 | | ... | 174 | | ... | 175 | | ... | 176 | | ... | 177 | | ... | 178 | | ... | 179 | | ... | 180 | | ... | line 180 181 | | ... | 182 | | ... | 183 | | ... | 184 | | ... | 185 | | ... | 186 | | ... | 187 | | ... | 188 | | ... | 189 | | ... | 190 | | ... | line 190 191 | | ... | 192 | | ... | 193 | | ... | 194 | | ... | 195 | | ... | 196 | | ... | 197 | | ... | 198 | | ... | 199 | | ... | 200 | | ... | line 200 201 | | ... | 202 | | ... | 203 | | ... | 204 | | ... | 205 | | ... | 206 | | ... | 207 | | ... | 208 | | ... | 209 | | ... | 210 | | ... | line 210 211 | | ... | 212 | | ... | 213 | | ... | 214 | | ... | 215 | | ... | 216 | | ... | 217 | | ... | 218 | | ... | 219 | | ... | 220 | | ... | line 220 221 | | ... | 222 | | ... | 223 | | ... | 224 | | ... | 225 | | ... | 226 | | ... | 227 | | ... | 228 | | ... | 229 | | ... | 230 | | ... | line 230 231 | | ... | 232 | | ... | 233 | | ... | 234 | | ... | 235 | | ... | 236 | | ... | 237 | | ... | 238 | | ... | 239 | | ... | 240 | | ... | line 240 241 | | ... | 242 | | ... | 243 | | ... | 244 | | ... | 245 | | ... | 246 | | ... | 247 | | ... | 248 | | ... | 249 | | ... | 250 | | ... | line 250 251 | | ... | 252 | | ... | 253 | | ... | 254 | | ... | 255 | | ... | 256 | | ... | 257 | | ... | 258 | | ... | 259 | | ... | 260 | | ... | line 260 261 | | ... | 262 | | ... | 263 | | ... | 264 | | ... | 265 | | ... | 266 | | ... | 267 | | ... | 268 | | ... | 269 | | ... | 270 | | ... | line 270 271 | | ... | 272 | | ... | 273 | | ... | 274 | | ... | 275 | | ... | 276 | | ... | 277 | | ... | 278 | | ... | 279 | | ... | 280 | | ... | line 280 281 | | ... | 282 | | ... | 283 | | ... | 284 | | ... | 285 | | ... | 286 | | ... | 287 | | ... | 288 | | ... | 289 | | ... | 290 | | ... | line 290 291 | | ... | 292 | | ... | 293 | | ... | 294 | | ... | 295 | | ... | 296 | | ... | 297 | | ... | 298 | | ... | 299 | | ... | 300 | | ... | line 300 301 | | ... | line 301 302 | -------------------------------------------------------------------------------- /rflint/parser/common.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import re 3 | 4 | class RobotStatements(object): 5 | def append(self, linenumber, raw_text, cells): 6 | """Add another row of data from a test suite""" 7 | self.rows.append(Row(linenumber, raw_text, cells)) 8 | 9 | @property 10 | def path(self): 11 | # this property exists so that the linter doesn't 12 | # have to have this logic 13 | return self.parent.path 14 | 15 | @property 16 | def steps(self): 17 | """Return a list of steps (statements that are not settings or comments)""" 18 | steps = [] 19 | for statement in self.statements: 20 | if ((not statement.is_comment()) and 21 | (not statement.is_setting())): 22 | steps.append(statement) 23 | return steps 24 | 25 | @property 26 | def settings(self): 27 | """Return a list of settings (statements with cell[1] matching \[.*?\]) 28 | 29 | Note: this returns any statement that *looks* like a setting. If you have 30 | a misspelled or completely bogus setting, it'll return that too 31 | (eg: | | [Blockumentation] | hello, world) 32 | """ 33 | return [statement for statement in self.statements 34 | if (statement.is_setting() and not statement.is_comment())] 35 | 36 | @property 37 | def statements(self): 38 | """Return a list of statements 39 | 40 | This is done by joining together any rows that 41 | have continuations 42 | """ 43 | # FIXME: no need to do this every time; we should cache the 44 | # result 45 | if len(self.rows) == 0: 46 | return [] 47 | 48 | current_statement = Statement(self.rows[0]) 49 | current_statement.startline = self.rows[0].linenumber 50 | current_statement.endline = self.rows[0].linenumber 51 | statements = [] 52 | for row in self.rows[1:]: 53 | if len(row) > 1 and row[0] == "" and row[1] == "...": 54 | # we found a continuation 55 | current_statement += row[2:] 56 | current_statement.endline = row.linenumber 57 | else: 58 | if len(current_statement) > 0: 59 | # append current statement to the list of statements... 60 | statements.append(current_statement) 61 | # start a new statement 62 | current_statement = Statement(row) 63 | current_statement.startline = row.linenumber 64 | current_statement.endline = row.linenumber 65 | 66 | if len(current_statement) > 0: 67 | statements.append(current_statement) 68 | 69 | return statements 70 | 71 | 72 | # TODO: make Row and Statement more similar -- either 73 | # both should inherit from list, or neither should. 74 | class Row(object): 75 | """A row is made up of a list of cells plus metadata""" 76 | def __init__(self, linenumber, raw_text, cells): 77 | self.linenumber = linenumber 78 | self.raw_text = raw_text 79 | self.cells = cells 80 | 81 | def dump(self): 82 | print("|" + " | ".join([cell.strip() for cell in self.cells])) 83 | def __len__(self): 84 | return len(self.cells) 85 | def __setitem__(self, key, value): 86 | self.cells[key] = value 87 | return self.cells[key] 88 | def __getitem__(self, key): 89 | return self.cells[key] 90 | def __repr__(self): 91 | return "" % (self.linenumber, str(self.cells)) 92 | def __contains__(self, key): 93 | return key in self.cells 94 | 95 | class Comment(Row): 96 | # this isn't entirely correct or well thought out. 97 | # I need a way to capture comments rather than 98 | # throw them away (mainly so I can recreate the original 99 | # file from the parsed data) 100 | pass 101 | 102 | class Statement(list): 103 | """A Statement is a list of cells, plus some metadata""" 104 | startline = None 105 | endline = None 106 | 107 | def is_setting(self): 108 | if ((len(self) > 1) and 109 | (re.match(r'\[.*?\]', self[1]))): 110 | return True 111 | return False 112 | 113 | def is_comment(self): 114 | '''Return True if the first non-empty cell starts with "#"''' 115 | 116 | for cell in self[:]: 117 | if cell == "": 118 | continue 119 | 120 | # this is the first non-empty cell. Check whether it is 121 | # a comment or not. 122 | if cell.lstrip().startswith("#"): 123 | return True 124 | else: 125 | return False 126 | return False 127 | 128 | def __repr__(self): 129 | return "(%.4s-%.4s)%s" % (self.startline, self.endline, list.__repr__(self)) 130 | -------------------------------------------------------------------------------- /rflint/rules/suiteRules.py: -------------------------------------------------------------------------------- 1 | from rflint.common import SuiteRule, ERROR, WARNING, normalize_name 2 | from rflint.parser import SettingTable 3 | import re 4 | 5 | class PeriodInSuiteName(SuiteRule): 6 | '''Warn about periods in the suite name 7 | 8 | Since robot uses "." as a path separator, using a "." in a suite 9 | name can lead to ambiguity. 10 | ''' 11 | severity = WARNING 12 | 13 | def apply(self,suite): 14 | if "." in suite.name: 15 | self.report(suite, "'.' in suite name '%s'" % suite.name, 0) 16 | 17 | class InvalidTable(SuiteRule): 18 | '''Verify that there are no invalid table headers 19 | 20 | Parameter robot_level to be set to 'robot3' (default) or 'robot2'.''' 21 | valid_tables_re = None 22 | default_robot_level = "robot3" 23 | 24 | def configure(self, robot_level): 25 | valid_tables = ['comments?', 'settings?', 'tasks?', 'test cases?', 26 | 'keywords?', 'variables?'] 27 | if robot_level == "robot2": 28 | valid_tables += ['cases?', 'metadata', 'user keywords?'] 29 | self.valid_tables_re = re.compile('^(' + '|'.join(valid_tables) + ')$', 30 | re.I) 31 | 32 | def apply(self, suite): 33 | if not self.valid_tables_re: 34 | self.configure(self.default_robot_level) 35 | for table in suite.tables: 36 | if not self.valid_tables_re.match(table.name): 37 | self.report(suite, "Unknown table name '%s'" % table.name, 38 | table.linenumber) 39 | 40 | 41 | class DuplicateKeywordNames(SuiteRule): 42 | '''Verify that no keywords have a name of an existing keyword in the same file''' 43 | severity = ERROR 44 | 45 | def apply(self, suite): 46 | cache = [] 47 | for keyword in suite.keywords: 48 | # normalize the name, so we catch things like 49 | # Smoke Test vs Smoke_Test, vs SmokeTest, which 50 | # robot thinks are all the same 51 | name = normalize_name(keyword.name) 52 | if name in cache: 53 | self.report(suite, "Duplicate keyword name '%s'" % keyword.name, keyword.linenumber) 54 | cache.append(name) 55 | 56 | class DuplicateTestNames(SuiteRule): 57 | '''Verify that no tests have a name of an existing test in the same suite''' 58 | severity = ERROR 59 | 60 | def apply(self, suite): 61 | cache = [] 62 | for testcase in suite.testcases: 63 | # normalize the name, so we catch things like 64 | # Smoke Test vs Smoke_Test, vs SmokeTest, which 65 | # robot thinks are all the same 66 | name = normalize_name(testcase.name) 67 | if name in cache: 68 | self.report(suite, "Duplicate testcase name '%s'" % testcase.name, testcase.linenumber) 69 | cache.append(name) 70 | 71 | class RequireSuiteDocumentation(SuiteRule): 72 | '''Verify that a test suite has documentation''' 73 | severity=WARNING 74 | 75 | def apply(self, suite): 76 | for table in suite.tables: 77 | if isinstance(table, SettingTable): 78 | for row in table.rows: 79 | if row[0].lower() == "documentation": 80 | return 81 | # we never found documentation; find the first line of the first 82 | # settings table, default to the first line of the file 83 | linenum = 1 84 | for table in suite.tables: 85 | if isinstance(table, SettingTable): 86 | linenum = table.linenumber + 1 87 | break 88 | 89 | self.report(suite, "No suite documentation", linenum) 90 | 91 | class TooManyTestCases(SuiteRule): 92 | ''' 93 | Should not have too many tests in one suite. 94 | 95 | The exception is if they are data-driven. 96 | 97 | https://code.google.com/p/robotframework/wiki/HowToWriteGoodTestCases#Test_suite_structure 98 | 99 | You can configure the maximum number of tests. The default is 10. 100 | ''' 101 | severity = WARNING 102 | max_allowed = 10 103 | 104 | def configure(self, max_allowed): 105 | self.max_allowed = int(max_allowed) 106 | 107 | def apply(self, suite): 108 | # check for template (data-driven tests) 109 | for table in suite.tables: 110 | if isinstance(table, SettingTable): 111 | for row in table.rows: 112 | if row[0].lower() == "test template": 113 | return 114 | # we didn't find a template, so these aren't data-driven 115 | testcases = list(suite.testcases) 116 | if len(testcases) > self.max_allowed: 117 | self.report( 118 | suite, "Too many test cases (%s > %s) in test suite" 119 | % (len(testcases), self.max_allowed), testcases[self.max_allowed].linenumber 120 | ) 121 | -------------------------------------------------------------------------------- /tests/acceptance/SharedKeywords.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Common keywords used by all the tests 3 | | Library | String 4 | | Library | Collections 5 | | Library | Process 6 | 7 | *** Keywords *** 8 | | Run rf-lint with the following options: 9 | | | [Arguments] | @{options} 10 | | | [Documentation] 11 | | | ... | Attempt to start the hub with the given options 12 | | | 13 | | | ... | The stdout of the process will be in a test suite 14 | | | ... | variable named \${output} 15 | | | 16 | | | ${python}= | Evaluate | sys.executable | sys | # use same python used to run the tests 17 | | | ${result}= | Run process | ${python} | -m | rflint | 18 | | | # Define a specific format for all messages (but can be overridden) 19 | | | ... | --format | {severity}: {linenumber}, {char}: {message} ({rulename}) 20 | | | ... | @{options} 21 | | | ... | output_encoding=utf-8 22 | | | Set test variable | ${result} 23 | | | 24 | | | log | stdout: ${result.stdout} | DEBUG 25 | | | log | stderr: ${result.stderr} | DEBUG 26 | 27 | | rflint return code should be 28 | | | [Documentation] 29 | | | ... | Validate the return code of the most recent run of rflint 30 | | | 31 | | | [Arguments] | ${expected} 32 | | | Should be equal as integers | ${result.rc} | ${expected} 33 | | | ... | Expected a result code of ${expected} but got ${result.rc} 34 | | | ... | values=False 35 | 36 | | rflint should report ${expected} errors 37 | | | [Documentation] 38 | | | ... | Verify that the output contains a specific number of errors. 39 | | | ... | 40 | | | ... | Note: this keyword assumes that the output format is 41 | | | ... | {severity}: {linenumber}, {char}: {message} ({rulename}) 42 | | | 43 | | | @{lines}= | Split to lines | ${result.stdout} 44 | | | ${actual}= | Get match count | ${lines} | regexp=^E: 45 | | | Run keyword if | ${actual} != ${expected} 46 | | | ... | log | ${result.stdout} 47 | | | Should be equal as numbers | ${expected} | ${actual} 48 | | | ... | Expected ${expected} errors but found ${actual} 49 | | | ... | values=False 50 | 51 | | rflint should report ${expected} warnings 52 | | | [Documentation] 53 | | | ... | Verify that the output contains a specific number of warings 54 | | | ... | 55 | | | ... | Note: this keyword assumes that the output format is 56 | | | ... | {severity}: {linenumber}, {char}: {message} ({rulename}) 57 | | | 58 | | | @{lines}= | Split to lines | ${result.stdout} 59 | | | ${actual}= | Get match count | ${lines} | regexp=^W: 60 | | | Should be equal as numbers | ${expected} | ${actual} 61 | | | ... | Expected ${expected} errors but found ${actual} 62 | 63 | | Output should contain 64 | | | [Arguments] | @{patterns} 65 | | | [Documentation] 66 | | | ... | Fail if the output from the previous command doesn't contain the given string 67 | | | ... | 68 | | | ... | This keyword assumes the output of the command is in 69 | | | ... | a test suite variable named \${result.stdout} 70 | | | ... | 71 | | | ... | To match against a regular expression, prefix the pattern with 'regexp=' 72 | | | ... | (this uses Collections.Should contain match to do the matching) 73 | | | ... | 74 | | | ... | Note: the help will be automatically wrapped, so 75 | | | ... | you can only search for relatively short strings. 76 | | | 77 | | | @{lines}= | Split to lines | ${result.stdout} 78 | | | log | ${lines} 79 | | | FOR | ${pattern} | IN | @{patterns} 80 | | | | Should contain match | ${lines} | ${pattern} 81 | | | | ... | expected:\n${pattern}\nbut got:\n${lines} 82 | | | END 83 | 84 | | Output should not contain 85 | | | [Arguments] | @{patterns} 86 | | | [Documentation] 87 | | | ... | Fail if the output from the previous command contains the given string 88 | | | ... | 89 | | | ... | This keyword assumes the output of the command is in 90 | | | ... | a test suite variable named \${result.stdout} 91 | | | ... | 92 | | | ... | Note: the help will be automatically wrapped, so 93 | | | ... | you can only search for relatively short strings. 94 | | | 95 | | | ${lines}= | Split to lines | ${result.stdout} 96 | | | FOR | ${pattern} | IN | @{patterns} 97 | | | | Should not contain match | ${lines} | ${pattern} 98 | | | | ... | stdout should not contain '${pattern}' but it did:\n${result.stdout} 99 | | | | ... | values=False 100 | | | END 101 | 102 | | Stdout should be 103 | | | [Arguments] | @{lines} 104 | | | [Documentation] 105 | | | ... | Verify that stdout of rflint matches the given set of lines. 106 | | | ... | All arguments are joined together with newlines 107 | | | 108 | | | ${expected}= | Catenate | SEPARATOR=\n | @{lines} 109 | | | Should be equal as strings | ${result.stdout} | ${expected} 110 | | | ... | Unexpected output on stdout.\nExpected:\n${expected}\nActual:\n${result.stdout} 111 | | | ... | values=False 112 | 113 | | Stderr should be 114 | | | [Arguments] | @{lines} 115 | | | [Documentation] 116 | | | ... | Verify that stderr of rflint matches the given set of lines. 117 | | | ... | All arguments are joined together with newlines 118 | | | 119 | | | ${expected}= | Catenate | SEPARATOR=\n | @{lines} 120 | | | Should be equal | ${result.stderr} | ${expected} 121 | | | ... | Unexpected output on stderr. \nExpected:\n${expected}\nActual:\n${result.stderr} 122 | | | ... | values=False 123 | -------------------------------------------------------------------------------- /rflint/parser/tables.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from .common import Statement 4 | 5 | class RobotTable(object): 6 | '''A table made up of zero or more rows''' 7 | def __init__(self, parent, linenumber=0, name=None, header=None): 8 | self.linenumber = linenumber 9 | self.name = name 10 | self.rows = [] 11 | self.comments = [] 12 | self.parent = parent 13 | self.header = header 14 | 15 | def dump(self): 16 | for row in self.rows: 17 | print("| " + " | ".join(row)) 18 | 19 | def append(self, row): 20 | self.rows.append(row) 21 | 22 | def __str__(self): 23 | if self.name is None: 24 | return "None" 25 | else: 26 | return self.name 27 | 28 | def __repr__(self): 29 | return "<%s(linenumbmer=%s, name=\"%s\")>" % (self.__class__.__name__, self.linenumber, self.name) 30 | 31 | class SimpleTableMixin(object): 32 | '''Mixin to handle simple tables (tables other than keywords and tests)''' 33 | 34 | @property 35 | def statements(self): 36 | '''Return a list of statements 37 | 38 | This is done by joining together any rows that 39 | have continuations 40 | ''' 41 | # FIXME: no need to do this every time; we should cache the 42 | # result 43 | if len(self.rows) == 0: 44 | return [] 45 | 46 | current_statement = Statement(self.rows[0]) 47 | current_statement.startline = self.rows[0].linenumber 48 | current_statement.endline = self.rows[0].linenumber 49 | statements = [] 50 | for row in self.rows[1:]: 51 | if len(row) > 0 and row[0] == "...": 52 | # we found a continuation 53 | current_statement += row[1:] 54 | current_statement.endline = row.linenumber 55 | else: 56 | if len(current_statement) > 0: 57 | # append current statement to the list of statements... 58 | statements.append(current_statement) 59 | # start a new statement 60 | current_statement = Statement(row) 61 | current_statement.startline = row.linenumber 62 | current_statement.endline = row.linenumber 63 | 64 | if len(current_statement) > 0: 65 | statements.append(current_statement) 66 | 67 | # trim trailing blank statements 68 | while (len(statements[-1]) == 0 or 69 | ((len(statements[-1]) == 1) and len(statements[-1][0]) == 0)): 70 | statements.pop() 71 | return statements 72 | 73 | class DefaultTable(RobotTable): pass # the table with no name 74 | class UnknownTable(RobotTable): pass # a table with an unknown header 75 | class SettingTable(RobotTable, SimpleTableMixin): pass 76 | class VariableTable(RobotTable): pass 77 | class MetadataTable(RobotTable): pass 78 | 79 | class AbstractContainerTable(RobotTable): 80 | '''Parent class of Keyword and Testcase tables''' 81 | _childClass = None 82 | def __init__(self, parent, *args, **kwargs): 83 | if self._childClass is None: 84 | # hey! Don't try to instantiate this class directly. 85 | raise Exception("D'oh! This is an abstract class.") 86 | super(AbstractContainerTable, self).__init__(parent, *args, **kwargs) 87 | self._children = [] 88 | self.parent = parent 89 | 90 | def dump(self): 91 | for child in self._children: 92 | print("| " + child.name) 93 | for row in child.rows: 94 | row.dump() 95 | 96 | def append(self, row): 97 | ''' 98 | The idea is, we recognize when we have a new testcase by 99 | checking the first cell. If it's not empty and not a comment, 100 | we have a new test case. 101 | 102 | ''' 103 | if len(row) == 0: 104 | # blank line. Should we throw it away, or append a BlankLine object? 105 | return 106 | 107 | if (row[0] != "" and 108 | (not row[0].lstrip().startswith("#"))): 109 | # we have a new child table 110 | self._children.append(self._childClass(self.parent, row.linenumber, row[0])) 111 | if len(row.cells) > 1: 112 | # It appears the first row -- which contains the test case or 113 | # keyword name -- also has the first logical row of cells. 114 | # We'll create a Row, but we'll make the first cell empty instead 115 | # of leaving the name in it, since other code always assumes the 116 | # first cell is empty. 117 | # 118 | # To be honest, I'm not sure this is the Right Thing To Do, but 119 | # I'm too lazy to audit the code to see if it matters if we keep 120 | # the first cell intact. Sorry if this ends up causing you grief 121 | # some day... 122 | row[0] = "" 123 | self._children[-1].append(row.linenumber, row.raw_text, row.cells) 124 | 125 | elif len(self._children) == 0: 126 | # something before the first test case 127 | # For now, append it to self.comments; eventually we should flag 128 | # an error if it's NOT a comment 129 | self.comments.append(row) 130 | 131 | else: 132 | # another row for the testcase 133 | if len(row.cells) > 0: 134 | self._children[-1].append(row.linenumber, row.raw_text, row.cells) 135 | 136 | 137 | -------------------------------------------------------------------------------- /tests/acceptance/smoke.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation 3 | | ... | This suite includes some very basic smoke tests for rflint 4 | | 5 | | Library | OperatingSystem 6 | | Library | Process 7 | | Library | SharedKeywords.py 8 | | Resource | SharedKeywords.robot 9 | | Force Tags | smoke 10 | | 11 | | Test Teardown 12 | | ... | Run keyword if | "${TEST STATUS}" == "FAIL" 13 | | ... | log | ${result.stdout}\n${result.stderr} 14 | 15 | 16 | *** Test Cases *** 17 | | Command line help 18 | | | [Documentation] 19 | | | ... | This test verifies that --help returns some useful information 20 | | | ... | 21 | | | ... | Not exactly an exhaustive test, but it at least 22 | | | ... | verifies that the command works. 23 | | | 24 | | | Run rf-lint with the following options: 25 | | | ... | --help 26 | | | 27 | | | rflint return code should be | 0 28 | | | # instead of doing an exhaustive test, let's just make 29 | | | # a quick spot-check 30 | | | Output should contain 31 | | | ... | usage:* 32 | | | ... | optional arguments: 33 | | | ... | *-h, --help* 34 | | | ... | *--error RULENAME, -e RULENAME* 35 | | | ... | *--ignore RULENAME, -i RULENAME* 36 | | | ... | *--warning RULENAME, -w RULENAME* 37 | | | ... | *--list* 38 | | | ... | *--rulefile RULEFILE, -R RULEFILE* 39 | | | ... | *--no-filenames* 40 | | | ... | *--format FORMAT, -f FORMAT* 41 | | | ... | *--recursive, -r* 42 | | | ... | *--argumentfile ARGUMENTFILE, -A ARGUMENTFILE* 43 | 44 | | | log | STDOUT:\n${result.stdout} 45 | | | log | STDERR:\n${result.stderr} 46 | 47 | | --list option 48 | | | [Documentation] 49 | | | ... | Verify that the --list option works. 50 | | | 51 | | | Run rf-lint with the following options: 52 | | | ... | --list 53 | | | rflint return code should be | 0 54 | | | log | STDOUT:\n${result.stdout} 55 | | | log | STDERR:\n${result.stderr} 56 | 57 | | --argumentfile option 58 | | | [Documentation] 59 | | | ... | Verify that the --argumentfile option correctly handles a good argument file 60 | | | [Setup] | Create file | ${TEMPDIR}/test.args | --ignore all\n 61 | | | 62 | | | run rf-lint with the following options: 63 | | | ... | --argumentfile | ${TEMPDIR}/test.args 64 | | | ... | --list 65 | | | # since each line of output begins with a single quote and then 66 | | | # the severity (gotta remove that stupid quote!), search for 67 | | | # lines that do _not_ begin with that. There should be none. 68 | | | ${lines}= | Get lines matching regexp | ${result.stdout} | ^'[^I]' 69 | | | length should be | ${lines} | 0 70 | | | ... | Not all rules are being ignored. Bummer? You bet! 71 | | | 72 | | | [Teardown] | Remove file | ${TEMPDIR}/test.args 73 | 74 | | rflint this file 75 | | | [Documentation] 76 | | | ... | Run rflint against this test suite 77 | | | 78 | | | Run rf-lint with the following options: 79 | | | ... | --format | {severity}: {linenumber}, {char}: {message} ({rulename}) 80 | | | ... | --no-filenames 81 | | | ... | ${SUITE_SOURCE} 82 | | | rflint return code should be | 0 83 | | | rflint should report 0 errors 84 | | | rflint should report 0 warnings 85 | 86 | | this file, converted to TSV 87 | | | [Documentation] 88 | | | ... | Run rflint against this file in .tsv format 89 | | | ... | 90 | | | ... | Note: robotidy likes to make really long lines with lots of 91 | | | ... | trailing whitespace, so we need to turn off a couple of rules 92 | | | [Setup] | Convert ${SUITE_SOURCE} to .tsv 93 | | | Run rf-lint with the following options: 94 | | | ... | --ignore | TrailingWhitespace 95 | | | ... | --ignore | LineTooLong 96 | | | ... | ${TEMPDIR}/smoke.tsv 97 | | | rflint return code should be | 0 98 | | | rflint should report 0 errors 99 | | | rflint should report 0 warnings 100 | | | [Teardown] | Run keyword if | ${result.rc} == 0 | Remove file | ${TEMPDIR}/smoke.tsv 101 | 102 | | this file, converted to space-separated format 103 | | | [Documentation] 104 | | | ... | Run rflint against this file in space separated format 105 | | | ... | 106 | | | ... | Note: robotidy likes to make really long lines with lots of 107 | | | ... | trailing whitespace, so we need to turn off a couple of rules 108 | | | [Setup] | Convert ${SUITE_SOURCE} to .txt 109 | | | run rf-lint with the following options: 110 | | | ... | --ignore | LineTooLong 111 | | | ... | ${TEMPDIR}/smoke.txt 112 | | | rflint return code should be | 0 113 | | | rflint should report 0 errors 114 | | | rflint should report 0 warnings 115 | | | [Teardown] | Run keyword if | ${result.rc} == 0 116 | | | ... | Remove file | ${TEMPDIR}/smoke.txt 117 | 118 | | non-zero exit code on failure 119 | | | [Documentation] 120 | | | ... | Validates that exit code is non-zero if errors are present 121 | | | 122 | | | [Setup] | Create a test suite | ${TEMPDIR}/busted.robot 123 | | | ... | *** Test Cases ***\n 124 | | | ... | An example test case\n 125 | | | ... | | comment | no documentation\n 126 | | | ... | | log | hello world 127 | | | 128 | | | Run rf-lint with the following options: 129 | | | ... | --ignore | all 130 | | | ... | --warn | RequireSuiteDocumentation 131 | | | ... | --error | RequireTestDocumentation 132 | | | ... | ${TEMPDIR}/busted.robot 133 | | | 134 | | | comment | there should be two errors: no suite documentation, no testcase documentation 135 | | | comment | but the return code is only a count of the errors 136 | | | rflint return code should be | 1 137 | | | rflint should report 1 errors 138 | | | rflint should report 1 warnings 139 | | | 140 | | | [Teardown] | Run keyword if | ${result.rc} == 1 141 | | | ... | Remove file | ${TEMPDIR}/busted.robot 142 | 143 | *** Keywords *** 144 | | Convert ${path} to ${format} 145 | | | [Documentation] 146 | | | ... | Converts this file to the given format, and save it to \${TEMPDIR} 147 | | | ... | Note: a type of .txt will be saved in the space-sepraated format. 148 | | | ... | 149 | | | ... | Example: 150 | | | ... | 151 | | | ... | Convert smoke.robot to .txt 152 | | | 153 | | | ${python}= | Evaluate | sys.executable | sys | # use same python used to run the tests 154 | | | ${outfile}= | Set variable | ${TEMPDIR}/smoke${format} 155 | | | ${result}= | Run process 156 | | | ... | ${python} | -m | robot.tidy | ${SUITE_SOURCE} | ${outfile} 157 | | | log | saving file as ${outfile} | DEBUG 158 | | | [return] | ${result} 159 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /rflint/parser/parser.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | A custom robotframework parser that retains line numbers (though 4 | it doesn't (yet!) retain character positions for each cell) 5 | 6 | 7 | Note: this only works on pipe and space separated files. It uses a 8 | copy of the deprecated TxtReader robot parser to divide a line into cells. 9 | 10 | (probably works for space-separated too. I haven't tried. ) 11 | 12 | Performance is pretty spiffy! At the time I write this (where 13 | admittedly I don't fully parse everything) it is about 3x-5x faster 14 | than the official robot parser. It can read a file with 500 15 | test cases and 500 keywords in about 30ms, compared to 150ms 16 | for the robot parser. Sweet. 17 | 18 | ''' 19 | from __future__ import print_function 20 | 21 | import re 22 | import sys 23 | import os.path 24 | from robot.errors import DataError 25 | from robot.utils import FileReader 26 | from .util import timeit, Matcher 27 | from .tables import AbstractContainerTable, DefaultTable, SettingTable, VariableTable, UnknownTable 28 | from .testcase import Testcase 29 | from .rfkeyword import Keyword 30 | from .common import Row, Statement 31 | 32 | 33 | def RobotFactory(path, parent=None): 34 | '''Return an instance of SuiteFile, ResourceFile, SuiteFolder 35 | 36 | Exactly which is returned depends on whether it's a file or 37 | folder, and if a file, the contents of the file. If there is a 38 | testcase table, this will return an instance of SuiteFile, 39 | otherwise it will return an instance of ResourceFile. 40 | ''' 41 | 42 | if os.path.isdir(path): 43 | return SuiteFolder(path, parent) 44 | 45 | else: 46 | rf = RobotFile(path, parent) 47 | 48 | for table in rf.tables: 49 | if isinstance(table, TestcaseTable): 50 | rf.__class__ = SuiteFile 51 | return rf 52 | 53 | rf.__class__ = ResourceFile 54 | return rf 55 | 56 | class SuiteFolder(object): 57 | def __init__(self, path, parent=None): 58 | 59 | self.path = os.path.abspath(path) 60 | self.parent = parent 61 | self.name = os.path.splitext(os.path.basename(path))[0] 62 | self.initfile = None 63 | 64 | # see if there's an initialization file. If so, 65 | # attempt to load it 66 | for filename in ("__init__.robot", "__init__.txt"): 67 | if os.path.exists(os.path.join(self.path, filename)): 68 | self.initfile = RobotFile(os.path.join(self.path, filename)) 69 | break 70 | 71 | 72 | def walk(self, *types): 73 | ''' 74 | Iterator which visits all suites and suite files, 75 | yielding test cases and keywords 76 | ''' 77 | requested = types if len(types) > 0 else [SuiteFile, ResourceFile, SuiteFolder, Testcase, Keyword] 78 | 79 | for thing in self.robot_files: 80 | if thing.__class__ in requested: 81 | yield thing 82 | if isinstance(thing, SuiteFolder): 83 | for child in thing.walk(): 84 | if child.__class__ in requested: 85 | yield child 86 | else: 87 | for child in thing.walk(*types): 88 | yield child 89 | 90 | @property 91 | def robot_files(self): 92 | '''Return a list of all folders, and test suite files (.txt, .robot) 93 | ''' 94 | result = [] 95 | for name in os.listdir(self.path): 96 | fullpath = os.path.join(self.path, name) 97 | if os.path.isdir(fullpath): 98 | result.append(RobotFactory(fullpath, parent=self)) 99 | else: 100 | if ((name.endswith(".txt") or name.endswith(".robot")) and 101 | (name not in ("__init__.txt", "__init__.robot"))): 102 | 103 | result.append(RobotFactory(fullpath, parent=self)) 104 | return result 105 | 106 | 107 | class RobotFile(object): 108 | ''' 109 | Terminology: 110 | 111 | - A file is a set of tables 112 | - A table begins with a heading and extends to the next table or EOF 113 | - Each table may be made up of smaller tables that define test cases 114 | or keywords 115 | - Each line of text in a table becomes a "Row". 116 | - A Row object contains a list of cells. 117 | - A cell is all of the data between pipes, stripped of leading and 118 | trailing spaces 119 | 120 | ''' 121 | def __init__(self, path, parent=None): 122 | self.parent = parent 123 | self.name = os.path.splitext(os.path.basename(path))[0] 124 | self.path = os.path.abspath(path) 125 | self.tables = [] 126 | self.rows = [] 127 | 128 | try: 129 | self._load(path) 130 | except Exception as e: 131 | sys.stderr.write("there was a problem reading '%s': %s\n" % (path, str(e))) 132 | 133 | def walk(self, *types): 134 | ''' 135 | Iterator which can return all test cases and/or keywords 136 | 137 | You can specify with objects to return as parameters; if 138 | no parameters are given, both tests and keywords will 139 | be returned. 140 | 141 | For example, to get only test cases, you could call it 142 | like this: 143 | 144 | robot_file = RobotFactory(...) 145 | for testcase in robot_file.walk(Testcase): ... 146 | 147 | ''' 148 | requested = types if len(types) > 0 else [Testcase, Keyword] 149 | 150 | if Testcase in requested: 151 | for testcase in self.testcases: 152 | yield testcase 153 | 154 | if Keyword in requested: 155 | for keyword in self.keywords: 156 | yield keyword 157 | 158 | def _load(self, path): 159 | 160 | ''' 161 | The general idea is to do a quick parse, creating a list of 162 | tables. Each table is nothing more than a list of rows, with 163 | each row being a list of cells. Additional parsing such as 164 | combining rows into statements is done on demand. This first 165 | pass is solely to read in the plain text and organize it by table. 166 | ''' 167 | 168 | self.tables = [] 169 | current_table = DefaultTable(self) 170 | 171 | with FileReader(path) as f: 172 | # N.B. the caller should be catching errors 173 | self.raw_text = f.read() 174 | f.file.seek(0) 175 | 176 | matcher = Matcher(re.IGNORECASE) 177 | for linenumber, raw_text in enumerate(f.readlines()): 178 | linenumber += 1; # start counting at 1 rather than zero 179 | 180 | # this mimics what the robot TSV reader does -- 181 | # it replaces non-breaking spaces with regular spaces, 182 | # and then strips trailing whitespace 183 | raw_text = raw_text.replace(u'\xA0', ' ') 184 | raw_text = raw_text.rstrip() 185 | 186 | # FIXME: I'm keeping line numbers but throwing away 187 | # where each cell starts. I should be preserving that 188 | # (though to be fair, robot is throwing that away so 189 | # I'll have to write my own splitter if I want to save 190 | # the character position) 191 | cells = self.split_row(raw_text) 192 | _heading_regex = r'^\s*\*+\s*(.*?)[ *]*$' 193 | 194 | if matcher(_heading_regex, cells[0]): 195 | # we've found the start of a new table 196 | table_name = matcher.group(1) 197 | current_table = tableFactory(self, linenumber, table_name, raw_text) 198 | self.tables.append(current_table) 199 | else: 200 | current_table.append(Row(linenumber, raw_text, cells)) 201 | 202 | def split_row(self, row): 203 | """ function copied from 204 | https://github.com/robotframework/robotframework/blob/v3.1.2/src/robot/parsing/robotreader.py 205 | """ 206 | space_splitter = re.compile(u'[ \t\xa0]{2,}|\t+') 207 | pipe_splitter = re.compile(u'[ \t\xa0]+\|(?=[ \t\xa0]+)') 208 | pipe_starts = ('|', '| ', '|\t', u'|\xa0') 209 | pipe_ends = (' |', '\t|', u'\xa0|') 210 | if row[:2] in pipe_starts: 211 | row = row[1:-1] if row[-2:] in pipe_ends else row[1:] 212 | return [cell.strip() 213 | for cell in pipe_splitter.split(row)] 214 | return space_splitter.split(row) 215 | 216 | def __repr__(self): 217 | return "" % self.path 218 | 219 | @property 220 | def type(self): 221 | '''Return 'suite' or 'resource' or None 222 | 223 | This will return 'suite' if a testcase table is found; 224 | It will return 'resource' if at least one robot table 225 | is found. If no tables are found it will return None 226 | ''' 227 | 228 | robot_tables = [table for table in self.tables if not isinstance(table, UnknownTable)] 229 | if len(robot_tables) == 0: 230 | return None 231 | 232 | for table in self.tables: 233 | if isinstance(table, TestcaseTable): 234 | return "suite" 235 | 236 | return "resource" 237 | 238 | @property 239 | def keywords(self): 240 | '''Generator which returns all keywords in the suite''' 241 | for table in self.tables: 242 | if isinstance(table, KeywordTable): 243 | for keyword in table.keywords: 244 | yield keyword 245 | 246 | @property 247 | def testcases(self): 248 | '''Generator which returns all test cases in the suite''' 249 | for table in self.tables: 250 | if isinstance(table, TestcaseTable): 251 | for testcase in table.testcases: 252 | yield testcase 253 | 254 | def dump(self): 255 | '''Regurgitate the tables and rows''' 256 | for table in self.tables: 257 | print("*** %s ***" % table.name) 258 | table.dump() 259 | 260 | 261 | def tableFactory(parent, linenumber, name, header): 262 | match = Matcher(re.IGNORECASE) 263 | if name is None: 264 | table = UnknownTable(parent, linenumber, name, header) 265 | elif match(r'settings?|metadata', name): 266 | table = SettingTable(parent, linenumber, name, header) 267 | elif match(r'variables?', name): 268 | table = VariableTable(parent, linenumber, name, header) 269 | elif match(r'test ?cases?', name): 270 | table = TestcaseTable(parent, linenumber, name, header) 271 | elif match(r'(user )?keywords?', name): 272 | table = KeywordTable(parent, linenumber, name, header) 273 | else: 274 | table = UnknownTable(parent, linenumber, name, header) 275 | 276 | return table 277 | 278 | 279 | class SuiteFile(RobotFile): 280 | def __repr__(self): 281 | return "" % self.path 282 | 283 | @property 284 | def settings(self): 285 | '''Generator which returns all of the statements in all of the settings tables''' 286 | for table in self.tables: 287 | if isinstance(table, SettingTable): 288 | for statement in table.statements: 289 | yield statement 290 | 291 | @property 292 | def variables(self): 293 | '''Generator which returns all of the statements in all of the variables tables''' 294 | for table in self.tables: 295 | if isinstance(table, VariableTable): 296 | # FIXME: settings have statements, variables have rows WTF? :-( 297 | for statement in table.rows: 298 | if statement[0] != "": 299 | yield statement 300 | 301 | class ResourceFile(RobotFile): 302 | def __repr__(self): 303 | return "" % self.path 304 | 305 | @property 306 | def settings(self): 307 | '''Generator which returns all of the statements in all of the settings tables''' 308 | for table in self.tables: 309 | if isinstance(table, SettingTable): 310 | for statement in table.statements: 311 | yield statement 312 | 313 | class TestcaseTable(AbstractContainerTable): 314 | _childClass = Testcase 315 | def __init__(self, parent, *args, **kwargs): 316 | super(TestcaseTable, self).__init__(parent, *args, **kwargs) 317 | self.testcases = self._children 318 | 319 | class KeywordTable(AbstractContainerTable): 320 | _childClass = Keyword 321 | def __init__(self, parent, *args, **kwargs): 322 | super(KeywordTable, self).__init__(parent, *args, **kwargs) 323 | self.keywords = self._children 324 | 325 | @timeit 326 | def dump(suite): 327 | result = [] 328 | for table in suite.tables: 329 | # print "table:", table 330 | # for row in table.rows: 331 | # print "=>", row 332 | if isinstance(table, TestcaseTable): 333 | for tc in table.testcases: 334 | # force parsing of individual steps 335 | steps = [step for step in tc.steps] 336 | 337 | if __name__ == "__main__": 338 | from robot.parsing import TestData, ResourceFile 339 | import sys 340 | 341 | # parse with the robot parser and this parser, to 342 | # see which is faster. Of course, this parser will 343 | # be faster :-) 344 | @timeit 345 | def test_robot(): 346 | try: 347 | suite = TestData(parent=None, source=sys.argv[1]) 348 | except DataError: 349 | # if loading the suite failed, assume it's a resource file 350 | # (bad assumption, but _whatever_) 351 | suite = ResourceFile(source=sys.argv[1]) 352 | return suite 353 | 354 | @timeit 355 | def test_mine(): 356 | suite = RobotFile(sys.argv[1]) 357 | # force parsing of every line 358 | for tc in suite.testcases: 359 | statements = tc.statements 360 | tags = tc.tags 361 | return suite 362 | 363 | if len(sys.argv) == 1: 364 | print("give me a filename on the command line") 365 | sys.exit(1) 366 | 367 | suite1 = test_robot() 368 | suite2 = test_mine() 369 | -------------------------------------------------------------------------------- /rflint/rflint.py: -------------------------------------------------------------------------------- 1 | """ 2 | rflint - a lint-like tool for robot framework plain text files 3 | 4 | Copyright 2014-2015 Bryan Oakley 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | """ 19 | from __future__ import print_function 20 | 21 | import os 22 | import sys 23 | import glob 24 | import argparse 25 | import imp 26 | 27 | from .common import SuiteRule, ResourceRule, TestRule, KeywordRule, GeneralRule, Rule 28 | from .common import ERROR, WARNING, IGNORE 29 | from .version import __version__ 30 | from .parser import RobotFactory, SuiteFile, ResourceFile 31 | from .exceptions import UnknownRuleException 32 | 33 | from robot.utils.argumentparser import ArgFileParser 34 | 35 | # Used to track which files have already been imported 36 | IMPORTED_RULE_FILES = [] 37 | 38 | 39 | class RfLint(object): 40 | """Robot Framework Linter""" 41 | 42 | def __init__(self): 43 | here = os.path.abspath(os.path.dirname(__file__)) 44 | builtin_rules = os.path.join(here, "rules") 45 | site_rules = os.path.join(here, "site-rules") 46 | 47 | # mapping of class names to instances, to enable us to 48 | # instantiate each rule exactly once 49 | self._rules = {} 50 | 51 | for path in (builtin_rules, site_rules): 52 | for filename in glob.glob(path+"/*.py"): 53 | if filename.endswith(".__init__.py"): 54 | continue 55 | self._load_rule_file(filename) 56 | 57 | @property 58 | def suite_rules(self): 59 | return self._get_rules(SuiteRule) 60 | 61 | @property 62 | def resource_rules(self): 63 | return self._get_rules(ResourceRule) 64 | 65 | @property 66 | def testcase_rules(self): 67 | return self._get_rules(TestRule) 68 | 69 | @property 70 | def keyword_rules(self): 71 | return self._get_rules(KeywordRule) 72 | 73 | @property 74 | def general_rules(self): 75 | return self._get_rules(GeneralRule) 76 | 77 | @property 78 | def all_rules(self): 79 | all = self.suite_rules + self.resource_rules + self.testcase_rules + self.keyword_rules + self.general_rules 80 | return all 81 | 82 | def run(self, args): 83 | """Parse command line arguments, and run rflint""" 84 | 85 | self.args = self.parse_and_process_args(args) 86 | 87 | if self.args.version: 88 | print(__version__) 89 | return 0 90 | 91 | if self.args.rulefile: 92 | for filename in self.args.rulefile: 93 | self._load_rule_file(filename) 94 | 95 | if self.args.list: 96 | self.list_rules() 97 | return 0 98 | 99 | if self.args.describe: 100 | self._describe_rules(self.args.args) 101 | return 0 102 | 103 | self.counts = { ERROR: 0, WARNING: 0, "other": 0} 104 | 105 | for filename in self.args.args: 106 | if not (os.path.exists(filename)): 107 | sys.stderr.write("rflint: %s: No such file or directory\n" % filename) 108 | continue 109 | if os.path.isdir(filename): 110 | self._process_folder(filename) 111 | else: 112 | self._process_file(filename) 113 | 114 | if self.counts[ERROR] > 0: 115 | return self.counts[ERROR] if self.counts[ERROR] < 254 else 255 116 | 117 | return 0 118 | 119 | def _is_valid_rule(self, rule_name): 120 | for rule in self.all_rules: 121 | if rule_name.lower() == rule.name.lower(): 122 | return True 123 | return False 124 | 125 | def _describe_rules(self, rule_names): 126 | for rulename in rule_names: 127 | if not self._is_valid_rule(rulename): 128 | raise UnknownRuleException(rulename) 129 | 130 | requested_rules = [rule.strip().lower() for rule in rule_names] 131 | for rule in sorted(self.all_rules, key=lambda rule: rule.name): 132 | if rule.name.lower() in requested_rules or len(requested_rules) == 0: 133 | print(rule.name) 134 | for line in rule.doc.splitlines(): 135 | print(" " + line) 136 | 137 | def _process_folder(self, path): 138 | if self.args.recursive: 139 | for root, subdirs, filenames in os.walk(path): 140 | self._process_files(root, filenames) 141 | else: 142 | self._process_files(path, os.listdir(path)) 143 | 144 | def _process_files(self, folder, filenames): 145 | for filename in filenames: 146 | name, ext = os.path.splitext(filename) 147 | if ext.lower() in (".robot", ".txt", ".tsv", ".resource"): 148 | self._process_file(os.path.join(folder, filename)) 149 | 150 | def _process_file(self, filename): 151 | # this is used by the reporting mechanism to know if it 152 | # should print the filename. Once it has been printed it 153 | # will be reset so that it won't get printed again until 154 | # we process the next file. 155 | self._print_filename = filename if self.args.print_filenames else None 156 | 157 | robot_file = RobotFactory(filename) 158 | for rule in self.general_rules: 159 | if rule.severity != IGNORE: 160 | rule.apply(robot_file) 161 | 162 | if isinstance(robot_file, SuiteFile): 163 | for rule in self.suite_rules: 164 | if rule.severity != IGNORE: 165 | rule.apply(robot_file) 166 | for testcase in robot_file.testcases: 167 | for rule in self.testcase_rules: 168 | if rule.severity != IGNORE: 169 | rule.apply(testcase) 170 | 171 | if isinstance(robot_file, ResourceFile): 172 | for rule in self.resource_rules: 173 | if rule.severity != IGNORE: 174 | rule.apply(robot_file) 175 | 176 | for keyword in robot_file.keywords: 177 | for rule in self.keyword_rules: 178 | if rule.severity != IGNORE: 179 | rule.apply(keyword) 180 | 181 | def list_rules(self): 182 | """Print a list of all rules""" 183 | for rule in sorted(self.all_rules, key=lambda rule: rule.name): 184 | print(rule) 185 | if self.args.verbose: 186 | for line in rule.doc.splitlines(): 187 | print(" ", line) 188 | 189 | def report(self, linenumber, filename, severity, message, rulename, char): 190 | """Report a rule violation""" 191 | 192 | if self._print_filename is not None: 193 | # we print the filename only once. self._print_filename 194 | # will get reset each time a new file is processed. 195 | print("+ " + self._print_filename) 196 | self._print_filename = None 197 | 198 | if severity in (WARNING, ERROR): 199 | self.counts[severity] += 1 200 | else: 201 | self.counts["other"] += 1 202 | 203 | if sys.version_info[0] == 2: 204 | # I _really_ hate doing this, but I can't figure out a 205 | # better way to handle unicode such that it works both 206 | # in python 2 and 3. There must be a better way, but 207 | # my unicode fu is weak. 208 | message = message.encode('utf-8') 209 | 210 | print(self.args.format.format(linenumber=linenumber, filename=filename, 211 | severity=severity, message=message, 212 | rulename=rulename, char=char)) 213 | 214 | def _get_rules(self, cls): 215 | """Returns a list of rules of a given class 216 | 217 | Rules are treated as singletons - we only instantiate each 218 | rule once. 219 | """ 220 | 221 | result = [] 222 | for rule_class in cls.__subclasses__(): 223 | rule_name = rule_class.__name__.lower() 224 | if rule_name not in self._rules: 225 | rule = rule_class(self) 226 | self._rules[rule_name] = rule 227 | result.append(self._rules[rule_name]) 228 | return result 229 | 230 | def _load_rule_file(self, filename): 231 | """Import the given rule file""" 232 | abspath = os.path.abspath(filename) 233 | if abspath in IMPORTED_RULE_FILES: 234 | return 235 | if not (os.path.exists(filename)): 236 | sys.stderr.write("rflint: %s: No such file or directory\n" % filename) 237 | return 238 | try: 239 | basename = os.path.basename(filename) 240 | (name, ext) = os.path.splitext(basename) 241 | imp.load_source(name, filename) 242 | IMPORTED_RULE_FILES.append(abspath) 243 | except Exception as e: 244 | sys.stderr.write("rflint: %s: exception while loading: %s\n" % (filename, str(e))) 245 | 246 | def parse_and_process_args(self, args): 247 | """Handle the parsing of command line arguments.""" 248 | 249 | parser = argparse.ArgumentParser( 250 | prog="python -m rflint", 251 | description="A static analyzer for robot framework plain text files.", 252 | formatter_class=argparse.RawDescriptionHelpFormatter, 253 | epilog = ( 254 | "You can use 'all' in place of RULENAME to refer to all rules. \n" 255 | "\n" 256 | "For example: '--ignore all --warn DuplicateTestNames' will ignore all\n" 257 | "rules except DuplicateTestNames.\n" 258 | "\n" 259 | "FORMAT is a string that performs a substitution on the following \n" 260 | "patterns: {severity}, {linenumber}, {char}, {message}, and {rulename}.\n" 261 | "\n" 262 | "For example: --format 'line: {linenumber}: message: {message}'. \n" 263 | "\n" 264 | "ARGUMENTFILE is a filename with contents that match the format of \n" 265 | "standard robot framework argument files\n" 266 | "\n" 267 | "If you give a directory as an argument, all files in the directory\n" 268 | "with the suffix .txt, .robot, .resource, or .tsv will be processed. \n" 269 | "With the --recursive option, subfolders within the directory will \n" 270 | "also be processed." 271 | ) 272 | ) 273 | parser.add_argument("--error", "-e", metavar="RULENAME", action=SetErrorAction, 274 | help="Assign a severity of ERROR to the given RULENAME") 275 | parser.add_argument("--ignore", "-i", metavar="RULENAME", action=SetIgnoreAction, 276 | help="Ignore the given RULENAME") 277 | parser.add_argument("--warning", "-w", metavar="RULENAME", action=SetWarningAction, 278 | help="Assign a severity of WARNING for the given RULENAME") 279 | parser.add_argument("--list", "-l", action="store_true", 280 | help="show a list of known rules and exit") 281 | parser.add_argument("--describe", "-d", action="store_true", 282 | help="describe the given rules") 283 | parser.add_argument("--no-filenames", action="store_false", dest="print_filenames", 284 | default=True, 285 | help="suppress the printing of filenames") 286 | parser.add_argument("--format", "-f", 287 | help="Define the output format", 288 | default='{severity}: {linenumber}, {char}: {message} ({rulename})') 289 | parser.add_argument("--version", action="store_true", default=False, 290 | help="Display version number and exit") 291 | parser.add_argument("--verbose", "-v", action="store_true", default=False, 292 | help="Give verbose output") 293 | parser.add_argument("--configure", "-c", action=ConfigureAction, 294 | help="Configure a rule") 295 | parser.add_argument("--recursive", "-r", action="store_true", default=False, 296 | help="Recursively scan subfolders in a directory") 297 | parser.add_argument("--rulefile", "-R", action=RulefileAction, 298 | help="import additional rules from the given RULEFILE") 299 | parser.add_argument("--argumentfile", "-A", action=ArgfileLoader, 300 | help="read arguments from the given file") 301 | parser.add_argument('args', metavar="file", nargs=argparse.REMAINDER) 302 | 303 | # create a custom namespace, in which we can store a reference to 304 | # our rules. This lets the custom argument actions access the list 305 | # of rules 306 | ns = argparse.Namespace() 307 | setattr(ns, "app", self) 308 | args = parser.parse_args(args, ns) 309 | 310 | Rule.output_format = args.format 311 | 312 | return args 313 | 314 | 315 | class RulefileAction(argparse.Action): 316 | def __call__(self, parser, namespace, arg, option_string=None): 317 | app = getattr(namespace, "app") 318 | app._load_rule_file(arg) 319 | 320 | 321 | class ConfigureAction(argparse.Action): 322 | def __call__(self, parser, namespace, arg, option_string=None): 323 | rulename, argstring = arg.split(":", 1) 324 | args = argstring.split(":") 325 | app = getattr(namespace, "app") 326 | 327 | for rule in app.all_rules: 328 | if rulename == rule.name: 329 | rule.configure(*args) 330 | return 331 | raise UnknownRuleException(rulename) 332 | 333 | 334 | class SetStatusAction(argparse.Action): 335 | """Abstract class which provides a method for checking the rule name""" 336 | def check_rule_name(self, rulename, rules): 337 | if (rulename != "all" and 338 | rulename.lower() not in [rule.name.lower() for rule in rules]): 339 | raise UnknownRuleException(rulename) 340 | 341 | 342 | class SetWarningAction(SetStatusAction): 343 | """Called when the argument parser encounters --warning""" 344 | def __call__(self, parser, namespace, rulename, option_string = None): 345 | 346 | app = getattr(namespace, "app") 347 | self.check_rule_name(rulename, app.all_rules) 348 | 349 | for rule in app.all_rules: 350 | if rulename == rule.name or rulename == "all": 351 | rule.severity = WARNING 352 | 353 | 354 | class SetErrorAction(SetStatusAction): 355 | """Called when the argument parser encounters --error""" 356 | def __call__(self, parser, namespace, rulename, option_string = None): 357 | 358 | app = getattr(namespace, "app") 359 | self.check_rule_name(rulename, app.all_rules) 360 | 361 | for rule in app.all_rules: 362 | if rulename == rule.name or rulename == "all": 363 | rule.severity = ERROR 364 | 365 | 366 | class SetIgnoreAction(SetStatusAction): 367 | """Called when the argument parser encounters --ignore""" 368 | def __call__(self, parser, namespace, rulename, option_string = None): 369 | 370 | app = getattr(namespace, "app") 371 | self.check_rule_name(rulename, app.all_rules) 372 | 373 | for rule in app.all_rules: 374 | if rulename == rule.name or rulename == "all": 375 | rule.severity = IGNORE 376 | 377 | 378 | class ArgfileLoader(argparse.Action): 379 | """Called when the argument parser encounters --argumentfile""" 380 | def __call__ (self, parser, namespace, values, option_string = None): 381 | ap = ArgFileParser(["--argumentfile","-A"]) 382 | args = ap.process(["-A", values]) 383 | parser.parse_args(args, namespace) 384 | --------------------------------------------------------------------------------