summaryrefslogtreecommitdiffstats
path: root/tests/test_cli.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_cli.py')
-rw-r--r--tests/test_cli.py795
1 files changed, 795 insertions, 0 deletions
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..444f2f9
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,795 @@
+# Copyright (C) 2016 Adrien Vergé
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from io import StringIO
+import fcntl
+import locale
+import os
+import pty
+import shutil
+import sys
+import tempfile
+import unittest
+
+from tests.common import build_temp_workspace, temp_workspace
+
+from yamllint import cli
+from yamllint import config
+
+
+class RunContext:
+ """Context manager for ``cli.run()`` to capture exit code and streams."""
+
+ def __init__(self, case):
+ self.stdout = self.stderr = None
+ self._raises_ctx = case.assertRaises(SystemExit)
+
+ def __enter__(self):
+ self._raises_ctx.__enter__()
+ sys.stdout = self.outstream = StringIO()
+ sys.stderr = self.errstream = StringIO()
+ return self
+
+ def __exit__(self, *exc_info):
+ self.stdout, sys.stdout = self.outstream.getvalue(), sys.__stdout__
+ self.stderr, sys.stderr = self.errstream.getvalue(), sys.__stderr__
+ return self._raises_ctx.__exit__(*exc_info)
+
+ @property
+ def returncode(self):
+ return self._raises_ctx.exception.code
+
+
+# Check system's UTF-8 availability
+def utf8_available():
+ try:
+ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
+ locale.setlocale(locale.LC_ALL, (None, None))
+ return True
+ except locale.Error: # pragma: no cover
+ return False
+
+
+class CommandLineTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.wd = build_temp_workspace({
+ # .yaml file at root
+ 'a.yaml': '---\n'
+ '- 1 \n'
+ '- 2',
+ # file with only one warning
+ 'warn.yaml': 'key: value\n',
+ # .yml file at root
+ 'empty.yml': '',
+ # file in dir
+ 'sub/ok.yaml': '---\n'
+ 'key: value\n',
+ # directory that looks like a yaml file
+ 'sub/directory.yaml/not-yaml.txt': '',
+ 'sub/directory.yaml/empty.yml': '',
+ # file in very nested dir
+ 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml': '---\n'
+ 'key: value\n'
+ 'key: other value\n',
+ # empty dir
+ 'empty-dir': [],
+ # non-YAML file
+ 'no-yaml.json': '---\n'
+ 'key: value\n',
+ # non-ASCII chars
+ 'non-ascii/éçäγλνπ¥/utf-8': (
+ '---\n'
+ '- hétérogénéité\n'
+ '# 19.99 €\n'
+ '- お早う御座います。\n'
+ '# الأَبْجَدِيَّة العَرَبِيَّة\n').encode(),
+ # dos line endings yaml
+ 'dos.yml': '---\r\n'
+ 'dos: true',
+ # different key-ordering by locale
+ 'c.yaml': '---\n'
+ 'A: true\n'
+ 'a: true',
+ 'en.yaml': '---\n'
+ 'a: true\n'
+ 'A: true'
+ })
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ shutil.rmtree(cls.wd)
+
+ @unittest.skipIf(not utf8_available() and sys.version_info < (3, 7),
+ 'UTF-8 paths not supported')
+ def test_find_files_recursively(self):
+ conf = config.YamlLintConfig('extends: default')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')],
+ )
+
+ items = [os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'empty-dir')]
+ self.assertEqual(
+ sorted(cli.find_files_recursively(items, conf)),
+ [os.path.join(self.wd, 'sub/ok.yaml')],
+ )
+
+ items = [os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 's')]
+ self.assertEqual(
+ sorted(cli.find_files_recursively(items, conf)),
+ [os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml')],
+ )
+
+ items = [os.path.join(self.wd, 'sub'),
+ os.path.join(self.wd, '/etc/another/file')]
+ self.assertEqual(
+ sorted(cli.find_files_recursively(items, conf)),
+ [os.path.join(self.wd, '/etc/another/file'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/ok.yaml')],
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*.yaml\' \n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*.yml\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*.json\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'no-yaml.json')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 'no-yaml.json'),
+ os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'*.yaml\'\n'
+ ' - \'*\'\n'
+ ' - \'**\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 'no-yaml.json'),
+ os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+ conf = config.YamlLintConfig('extends: default\n'
+ 'yaml-files:\n'
+ ' - \'s/**\'\n'
+ ' - \'**/utf-8\'\n')
+ self.assertEqual(
+ sorted(cli.find_files_recursively([self.wd], conf)),
+ [os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8')]
+ )
+
+ def test_run_with_bad_arguments(self):
+ with RunContext(self) as ctx:
+ cli.run(())
+ self.assertNotEqual(ctx.returncode, 0)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^usage')
+
+ with RunContext(self) as ctx:
+ cli.run(('--unknown-arg', ))
+ self.assertNotEqual(ctx.returncode, 0)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^usage')
+
+ with RunContext(self) as ctx:
+ cli.run(('-c', './conf.yaml', '-d', 'relaxed', 'file'))
+ self.assertNotEqual(ctx.returncode, 0)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(
+ ctx.stderr.splitlines()[-1],
+ r'^yamllint: error: argument -d\/--config-data: '
+ r'not allowed with argument -c\/--config-file$'
+ )
+
+ # checks if reading from stdin and files are mutually exclusive
+ with RunContext(self) as ctx:
+ cli.run(('-', 'file'))
+ self.assertNotEqual(ctx.returncode, 0)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^usage')
+
+ def test_run_with_bad_config(self):
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules: {a: b}', 'file'))
+ self.assertEqual(ctx.returncode, -1)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^invalid config: no such rule')
+
+ def test_run_with_empty_config(self):
+ with RunContext(self) as ctx:
+ cli.run(('-d', '', 'file'))
+ self.assertEqual(ctx.returncode, -1)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'^invalid config: not a dict')
+
+ def test_run_with_implicit_extends_config(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'default', '-f', 'parsable', path))
+ expected_out = (f'{path}:1:1: [warning] missing document start "---" '
+ f'(document-start)\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (0, expected_out, ''))
+
+ def test_run_with_config_file(self):
+ with open(os.path.join(self.wd, 'config'), 'w') as f:
+ f.write('rules: {trailing-spaces: disable}')
+
+ with RunContext(self) as ctx:
+ cli.run(('-c', f.name, os.path.join(self.wd, 'a.yaml')))
+ self.assertEqual(ctx.returncode, 0)
+
+ with open(os.path.join(self.wd, 'config'), 'w') as f:
+ f.write('rules: {trailing-spaces: enable}')
+
+ with RunContext(self) as ctx:
+ cli.run(('-c', f.name, os.path.join(self.wd, 'a.yaml')))
+ self.assertEqual(ctx.returncode, 1)
+
+ @unittest.skipIf(os.environ.get('GITHUB_RUN_ID'), '$HOME not overridable')
+ def test_run_with_user_global_config_file(self):
+ home = os.path.join(self.wd, 'fake-home')
+ dir = os.path.join(home, '.config', 'yamllint')
+ os.makedirs(dir)
+ config = os.path.join(dir, 'config')
+
+ self.addCleanup(os.environ.update, HOME=os.environ['HOME'])
+ os.environ['HOME'] = home
+
+ with open(config, 'w') as f:
+ f.write('rules: {trailing-spaces: disable}')
+
+ with RunContext(self) as ctx:
+ cli.run((os.path.join(self.wd, 'a.yaml'), ))
+ self.assertEqual(ctx.returncode, 0)
+
+ with open(config, 'w') as f:
+ f.write('rules: {trailing-spaces: enable}')
+
+ with RunContext(self) as ctx:
+ cli.run((os.path.join(self.wd, 'a.yaml'), ))
+ self.assertEqual(ctx.returncode, 1)
+
+ def test_run_with_user_xdg_config_home_in_env(self):
+ self.addCleanup(os.environ.__delitem__, 'XDG_CONFIG_HOME')
+
+ with tempfile.TemporaryDirectory('w') as d:
+ os.environ['XDG_CONFIG_HOME'] = d
+ os.makedirs(os.path.join(d, 'yamllint'))
+ with open(os.path.join(d, 'yamllint', 'config'), 'w') as f:
+ f.write('extends: relaxed')
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', os.path.join(self.wd, 'warn.yaml')))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ def test_run_with_user_yamllint_config_file_in_env(self):
+ self.addCleanup(os.environ.__delitem__, 'YAMLLINT_CONFIG_FILE')
+
+ with tempfile.NamedTemporaryFile('w') as f:
+ os.environ['YAMLLINT_CONFIG_FILE'] = f.name
+ f.write('rules: {trailing-spaces: disable}')
+ f.flush()
+ with RunContext(self) as ctx:
+ cli.run((os.path.join(self.wd, 'a.yaml'), ))
+ self.assertEqual(ctx.returncode, 0)
+
+ with tempfile.NamedTemporaryFile('w') as f:
+ os.environ['YAMLLINT_CONFIG_FILE'] = f.name
+ f.write('rules: {trailing-spaces: enable}')
+ f.flush()
+ with RunContext(self) as ctx:
+ cli.run((os.path.join(self.wd, 'a.yaml'), ))
+ self.assertEqual(ctx.returncode, 1)
+
+ def test_run_with_locale(self):
+ # check for availability of locale, otherwise skip the test
+ # reset to default before running the test,
+ # as the first two runs don't use setlocale()
+ try:
+ locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
+ except locale.Error: # pragma: no cover
+ self.skipTest('locale en_US.UTF-8 not available')
+ locale.setlocale(locale.LC_ALL, (None, None))
+
+ # C + en.yaml should fail
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules: { key-ordering: enable }',
+ os.path.join(self.wd, 'en.yaml')))
+ self.assertEqual(ctx.returncode, 1)
+
+ # C + c.yaml should pass
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules: { key-ordering: enable }',
+ os.path.join(self.wd, 'c.yaml')))
+ self.assertEqual(ctx.returncode, 0)
+
+ # the next two runs use setlocale() inside,
+ # so we need to clean up afterwards
+ self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
+
+ # en_US + en.yaml should pass
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'locale: en_US.UTF-8\n'
+ 'rules: { key-ordering: enable }',
+ os.path.join(self.wd, 'en.yaml')))
+ self.assertEqual(ctx.returncode, 0)
+
+ # en_US + c.yaml should fail
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'locale: en_US.UTF-8\n'
+ 'rules: { key-ordering: enable }',
+ os.path.join(self.wd, 'c.yaml')))
+ self.assertEqual(ctx.returncode, 1)
+
+ def test_run_version(self):
+ with RunContext(self) as ctx:
+ cli.run(('--version', ))
+ self.assertEqual(ctx.returncode, 0)
+ self.assertRegex(ctx.stdout + ctx.stderr, r'yamllint \d+\.\d+')
+
+ def test_run_non_existing_file(self):
+ path = os.path.join(self.wd, 'i-do-not-exist.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual(ctx.returncode, -1)
+ self.assertEqual(ctx.stdout, '')
+ self.assertRegex(ctx.stderr, r'No such file or directory')
+
+ def test_run_one_problem_file(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual(ctx.returncode, 1)
+ self.assertEqual(ctx.stdout, (
+ f'{path}:2:4: [error] trailing spaces (trailing-spaces)\n'
+ f'{path}:3:4: [error] no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'))
+ self.assertEqual(ctx.stderr, '')
+
+ def test_run_one_warning(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual(ctx.returncode, 0)
+
+ def test_run_warning_in_strict_mode(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', '--strict', path))
+ self.assertEqual(ctx.returncode, 2)
+
+ def test_run_one_ok_file(self):
+ path = os.path.join(self.wd, 'sub', 'ok.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ def test_run_empty_file(self):
+ path = os.path.join(self.wd, 'empty.yml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ @unittest.skipIf(not utf8_available(), 'C.UTF-8 not available')
+ def test_run_non_ascii_file(self):
+ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
+ self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
+
+ path = os.path.join(self.wd, 'non-ascii', 'éçäγλνπ¥', 'utf-8')
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', path))
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ def test_run_multiple_files(self):
+ items = [os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 's')]
+ path = items[1] + '/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'
+
+ with RunContext(self) as ctx:
+ cli.run(['-f', 'parsable'] + items)
+ self.assertEqual((ctx.returncode, ctx.stderr), (1, ''))
+ self.assertEqual(ctx.stdout, (
+ f'{path}:3:1: [error] duplication of key "key" in mapping '
+ f'(key-duplicates)\n'))
+
+ def test_run_piped_output_nocolor(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, ))
+ self.assertEqual((ctx.returncode, ctx.stderr), (1, ''))
+ self.assertEqual(ctx.stdout, (
+ f'{path}\n'
+ f' 2:4 error trailing spaces (trailing-spaces)\n'
+ f' 3:4 error no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'
+ f'\n'))
+
+ def test_run_default_format_output_in_tty(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ # Create a pseudo-TTY and redirect stdout to it
+ master, slave = pty.openpty()
+ sys.stdout = sys.stderr = os.fdopen(slave, 'w')
+
+ with self.assertRaises(SystemExit) as ctx:
+ cli.run((path, ))
+ sys.stdout.flush()
+
+ self.assertEqual(ctx.exception.code, 1)
+
+ # Read output from TTY
+ output = os.fdopen(master, 'r')
+ flag = fcntl.fcntl(master, fcntl.F_GETFD)
+ fcntl.fcntl(master, fcntl.F_SETFL, flag | os.O_NONBLOCK)
+
+ out = output.read().replace('\r\n', '\n')
+
+ sys.stdout.close()
+ sys.stderr.close()
+ output.close()
+
+ self.assertEqual(out, (
+ f'\033[4m{path}\033[0m\n'
+ f' \033[2m2:4\033[0m \033[31merror\033[0m '
+ f'trailing spaces \033[2m(trailing-spaces)\033[0m\n'
+ f' \033[2m3:4\033[0m \033[31merror\033[0m '
+ f'no new line character at the end of file '
+ f'\033[2m(new-line-at-end-of-file)\033[0m\n'
+ f'\n'))
+
+ def test_run_default_format_output_without_tty(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, ))
+ expected_out = (
+ f'{path}\n'
+ f' 2:4 error trailing spaces (trailing-spaces)\n'
+ f' 3:4 error no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_auto_output_without_tty_output(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'auto'))
+ expected_out = (
+ f'{path}\n'
+ f' 2:4 error trailing spaces (trailing-spaces)\n'
+ f' 3:4 error no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_format_colored(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'colored'))
+ expected_out = (
+ f'\033[4m{path}\033[0m\n'
+ f' \033[2m2:4\033[0m \033[31merror\033[0m '
+ f'trailing spaces \033[2m(trailing-spaces)\033[0m\n'
+ f' \033[2m3:4\033[0m \033[31merror\033[0m '
+ f'no new line character at the end of file '
+ f'\033[2m(new-line-at-end-of-file)\033[0m\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_format_colored_warning(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'colored'))
+ expected_out = (
+ f'\033[4m{path}\033[0m\n'
+ f' \033[2m1:1\033[0m \033[33mwarning\033[0m '
+ f'missing document start "---" \033[2m(document-start)\033[0m\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (0, expected_out, ''))
+
+ def test_run_format_github(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--format', 'github'))
+ expected_out = (
+ f'::group::{path}\n'
+ f'::error file={path},line=2,col=4::2:4 [trailing-spaces] trailing'
+ f' spaces\n'
+ f'::error file={path},line=3,col=4::3:4 [new-line-at-end-of-file]'
+ f' no new line character at the end of file\n'
+ f'::endgroup::\n\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_github_actions_detection(self):
+ path = os.path.join(self.wd, 'a.yaml')
+ self.addCleanup(os.environ.__delitem__, 'GITHUB_ACTIONS')
+ self.addCleanup(os.environ.__delitem__, 'GITHUB_WORKFLOW')
+
+ with RunContext(self) as ctx:
+ os.environ['GITHUB_ACTIONS'] = 'something'
+ os.environ['GITHUB_WORKFLOW'] = 'something'
+ cli.run((path, ))
+ expected_out = (
+ f'::group::{path}\n'
+ f'::error file={path},line=2,col=4::2:4 [trailing-spaces] trailing'
+ f' spaces\n'
+ f'::error file={path},line=3,col=4::3:4 [new-line-at-end-of-file]'
+ f' no new line character at the end of file\n'
+ f'::endgroup::\n\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_read_from_stdin(self):
+ # prepares stdin with an invalid yaml string so that we can check
+ # for its specific error, and be assured that stdin was read
+ self.addCleanup(setattr, sys, 'stdin', sys.__stdin__)
+ sys.stdin = StringIO(
+ 'I am a string\n'
+ 'therefore: I am an error\n')
+
+ with RunContext(self) as ctx:
+ cli.run(('-', '-f', 'parsable'))
+ expected_out = (
+ 'stdin:2:10: [error] syntax error: '
+ 'mapping values are not allowed here (syntax)\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_no_warnings(self):
+ path = os.path.join(self.wd, 'a.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--no-warnings', '-f', 'auto'))
+ expected_out = (
+ f'{path}\n'
+ f' 2:4 error trailing spaces (trailing-spaces)\n'
+ f' 3:4 error no new line character at the end of file '
+ f'(new-line-at-end-of-file)\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--no-warnings', '-f', 'auto'))
+ self.assertEqual(ctx.returncode, 0)
+
+ def test_run_no_warnings_and_strict(self):
+ path = os.path.join(self.wd, 'warn.yaml')
+
+ with RunContext(self) as ctx:
+ cli.run((path, '--no-warnings', '-s'))
+ self.assertEqual(ctx.returncode, 2)
+
+ def test_run_non_universal_newline(self):
+ path = os.path.join(self.wd, 'dos.yml')
+
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules:\n new-lines:\n type: dos', path))
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
+
+ with RunContext(self) as ctx:
+ cli.run(('-d', 'rules:\n new-lines:\n type: unix', path))
+ expected_out = (
+ f'{path}\n'
+ f' 1:4 error wrong new line character: expected \\n'
+ f' (new-lines)\n'
+ f'\n')
+ self.assertEqual(
+ (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
+
+ def test_run_list_files(self):
+ with RunContext(self) as ctx:
+ cli.run(('--list-files', self.wd))
+ self.assertEqual(ctx.returncode, 0)
+ self.assertEqual(
+ sorted(ctx.stdout.splitlines()),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'dos.yml'),
+ os.path.join(self.wd, 'empty.yml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+ config = '{ignore: "*.yml", yaml-files: ["*.*"]}'
+ with RunContext(self) as ctx:
+ cli.run(('--list-files', '-d', config, self.wd))
+ self.assertEqual(ctx.returncode, 0)
+ self.assertEqual(
+ sorted(ctx.stdout.splitlines()),
+ [os.path.join(self.wd, 'a.yaml'),
+ os.path.join(self.wd, 'c.yaml'),
+ os.path.join(self.wd, 'en.yaml'),
+ os.path.join(self.wd, 'no-yaml.json'),
+ os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
+ os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
+ os.path.join(self.wd, 'sub/ok.yaml'),
+ os.path.join(self.wd, 'warn.yaml')]
+ )
+
+
+class CommandLineConfigTestCase(unittest.TestCase):
+ def test_config_file(self):
+ workspace = {'a.yml': 'hello: world\n'}
+ conf = ('---\n'
+ 'extends: relaxed\n')
+
+ for conf_file in ('.yamllint', '.yamllint.yml', '.yamllint.yaml'):
+ with self.subTest(conf_file):
+ with temp_workspace(workspace):
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, './a.yml:1:1: [warning] missing document '
+ 'start "---" (document-start)\n', ''))
+
+ with temp_workspace({**workspace, **{conf_file: conf}}):
+ with RunContext(self) as ctx:
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, '', ''))
+
+ def test_parent_config_file(self):
+ workspace = {'a/b/c/d/e/f/g/a.yml': 'hello: world\n'}
+ conf = ('---\n'
+ 'extends: relaxed\n')
+
+ for conf_file in ('.yamllint', '.yamllint.yml', '.yamllint.yaml'):
+ with self.subTest(conf_file):
+ with temp_workspace(workspace):
+ with RunContext(self) as ctx:
+ os.chdir('a/b/c/d/e/f')
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, './g/a.yml:1:1: [warning] missing '
+ 'document start "---" (document-start)\n',
+ ''))
+
+ with temp_workspace({**workspace, **{conf_file: conf}}):
+ with RunContext(self) as ctx:
+ os.chdir('a/b/c/d/e/f')
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, '', ''))
+
+ def test_multiple_parent_config_file(self):
+ workspace = {'a/b/c/3spaces.yml': 'array:\n'
+ ' - item\n',
+ 'a/b/c/4spaces.yml': 'array:\n'
+ ' - item\n',
+ 'a/.yamllint': '---\n'
+ 'extends: relaxed\n'
+ 'rules:\n'
+ ' indentation:\n'
+ ' spaces: 4\n',
+ }
+
+ conf3 = ('---\n'
+ 'extends: relaxed\n'
+ 'rules:\n'
+ ' indentation:\n'
+ ' spaces: 3\n')
+
+ with temp_workspace(workspace):
+ with RunContext(self) as ctx:
+ os.chdir('a/b/c')
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, './3spaces.yml:2:4: [warning] wrong indentation: '
+ 'expected 4 but found 3 (indentation)\n', ''))
+
+ with temp_workspace({**workspace, **{'a/b/.yamllint.yml': conf3}}):
+ with RunContext(self) as ctx:
+ os.chdir('a/b/c')
+ cli.run(('-f', 'parsable', '.'))
+
+ self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr),
+ (0, './4spaces.yml:2:5: [warning] wrong indentation: '
+ 'expected 3 but found 4 (indentation)\n', ''))