-
-
Notifications
You must be signed in to change notification settings - Fork 186
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #42 from pelson/moving_ahead
Added a recipe linter for common problems with recipes.
- Loading branch information
Showing
3 changed files
with
183 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import os | ||
|
||
import ruamel.yaml | ||
|
||
|
||
EXPECTED_SECTION_ORDER = ['package', 'source', 'build', 'requirements', 'test', 'app', 'about', 'extra'] | ||
|
||
|
||
def lintify(meta): | ||
lints = [] | ||
|
||
major_sections = list(meta.keys()) | ||
|
||
# 1: Top level meta.yaml keys should have a specific order. | ||
section_order_sorted = sorted(major_sections, key=EXPECTED_SECTION_ORDER.index) | ||
if major_sections != section_order_sorted: | ||
lints.append('The top level meta keys are in an unexpected order. Expecting {}.'.format(section_order_sorted)) | ||
|
||
# 2: The about section should have a home, license and summary. | ||
for about_item in ['home', 'license', 'summary']: | ||
about_section = meta.get('about', {}) or {} | ||
# if the section doesn't exist, or is just empty, lint it. | ||
if not about_section.get(about_item, ''): | ||
lints.append('The {} item is expected in the about section.'.format(about_item)) | ||
|
||
# 3: The recipe should have some maintainers. | ||
extra_section = meta.get('extra', {}) or {} | ||
if not extra_section.get('recipe-maintainers', []): | ||
lints.append('The recipe could do with some maintainers listed in the "extra/recipe-maintainers" section.') | ||
|
||
# 4: The recipe should have some tests. | ||
if 'test' not in major_sections: | ||
lints.append('The recipe must have some tests.') | ||
|
||
return lints | ||
|
||
|
||
def main(recipe_dir): | ||
recipe_dir = os.path.abspath(recipe_dir) | ||
recipe_meta = os.path.join(recipe_dir, 'meta.yaml') | ||
if not os.path.exists(recipe_dir): | ||
raise IOError('Feedstock has no recipe/meta.yaml.') | ||
with open(recipe_meta, 'r') as fh: | ||
meta = ruamel.yaml.load(fh, ruamel.yaml.RoundTripLoader) | ||
results = lintify(meta) | ||
return results |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
from __future__ import print_function | ||
from collections import OrderedDict | ||
import os | ||
import shutil | ||
import subprocess | ||
import tempfile | ||
import textwrap | ||
import unittest | ||
|
||
import conda_smithy.lint_recipe as linter | ||
|
||
|
||
class Test_linter(unittest.TestCase): | ||
def test_bad_order(self): | ||
meta = OrderedDict([['package', []], | ||
['build', []], | ||
['source', []]]) | ||
lints = linter.lintify(meta) | ||
expected_message = "The top level meta keys are in an unexpected order. Expecting ['package', 'source', 'build']." | ||
self.assertIn(expected_message, lints) | ||
|
||
def test_missing_about_license_and_summary(self): | ||
meta = {'about': {'home': 'a URL'}} | ||
lints = linter.lintify(meta) | ||
expected_message = "The license item is expected in the about section." | ||
self.assertIn(expected_message, lints) | ||
|
||
expected_message = "The summary item is expected in the about section." | ||
self.assertIn(expected_message, lints) | ||
|
||
def test_missing_about_home(self): | ||
meta = {'about': {'license': 'BSD', | ||
'summary': 'A test summary'}} | ||
lints = linter.lintify(meta) | ||
expected_message = "The home item is expected in the about section." | ||
self.assertIn(expected_message, lints) | ||
|
||
def test_missing_about_home_empty(self): | ||
meta = {'about': {'home': '', | ||
'summary': '', | ||
'license': ''}} | ||
lints = linter.lintify(meta) | ||
expected_message = "The home item is expected in the about section." | ||
self.assertIn(expected_message, lints) | ||
|
||
expected_message = "The license item is expected in the about section." | ||
self.assertIn(expected_message, lints) | ||
|
||
expected_message = "The summary item is expected in the about section." | ||
self.assertIn(expected_message, lints) | ||
|
||
def test_maintainers_section(self): | ||
expected_message = 'The recipe could do with some maintainers listed in the "extra/recipe-maintainers" section.' | ||
|
||
lints = linter.lintify({'extra': {'recipe-maintainers': []}}) | ||
self.assertIn(expected_message, lints) | ||
|
||
# No extra section at all. | ||
lints = linter.lintify({}) | ||
self.assertIn(expected_message, lints) | ||
|
||
lints = linter.lintify({'extra': {'recipe-maintainers': ['a']}}) | ||
self.assertNotIn(expected_message, lints) | ||
|
||
def test_test_section(self): | ||
expected_message = 'The recipe must have some tests.' | ||
|
||
lints = linter.lintify({}) | ||
self.assertIn(expected_message, lints) | ||
|
||
lints = linter.lintify({'test': {'imports': 'sys'}}) | ||
self.assertNotIn(expected_message, lints) | ||
|
||
|
||
class TestCLI_recipe_lint(unittest.TestCase): | ||
def setUp(self): | ||
self.tmp_dir = tempfile.mkdtemp('recipe_') | ||
|
||
def tearDown(self): | ||
shutil.rmtree(self.tmp_dir) | ||
|
||
def test_cli_fail(self): | ||
with open(os.path.join(self.tmp_dir, 'meta.yaml'), 'w') as fh: | ||
fh.write(textwrap.dedent(""" | ||
package: | ||
name: 'test_package' | ||
build: [] | ||
requirements: [] | ||
""")) | ||
child = subprocess.Popen(['conda-smithy', 'recipe-lint', self.tmp_dir], | ||
stdout=subprocess.PIPE) | ||
child.communicate() | ||
self.assertEqual(child.returncode, 1) | ||
|
||
def test_cli_success(self): | ||
with open(os.path.join(self.tmp_dir, 'meta.yaml'), 'w') as fh: | ||
fh.write(textwrap.dedent(""" | ||
package: | ||
name: 'test_package' | ||
test: [] | ||
about: | ||
home: something | ||
license: something else | ||
summary: a test recipe | ||
extra: | ||
recipe-maintainers: | ||
- a | ||
- b | ||
""")) | ||
child = subprocess.Popen(['conda-smithy', 'recipe-lint', self.tmp_dir], | ||
stdout=subprocess.PIPE) | ||
child.communicate() | ||
self.assertEqual(child.returncode, 0) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |