Improve our CI tests (#11476)

* add a test and dry-run to qmk generate-api

* add a dry-run to qmk pyformat

* Add a --dry-run to qmk cformat

* reverse the order of nose2 and flake8 tests

* run CI test against cformat and pyformat

* fix programming errors

* tweak job name

* fix argument

* refine the files we select

* fix stack trace in --ci

* make cformat exit clean

* fix c file extensions

* decouple CI from pyformat

* remove --ci arg

* make ci happy

* use the environment var instead

* change output to text

* fix log message

* replace tabs
master
Zach White 2021-05-10 11:18:44 -07:00 committed by GitHub
parent 66ed80ad3a
commit a3e7f3e7c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 170 additions and 83 deletions

View File

@ -1,47 +1,42 @@
name: Format Codebase name: PR Lint Format
on: on:
push: pull_request:
branches: paths:
- master - 'drivers/**'
- develop - 'lib/arm_atsam/**'
- 'lib/lib8tion/**'
- 'lib/python/**'
- 'platforms/**'
- 'quantum/**'
- 'tests/**'
- 'tmk_core/**'
jobs: jobs:
format: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: qmkfm/base_container
# protect against those who develop with their fork on master container: qmkfm/base_container
if: github.repository == 'qmk/qmk_firmware'
steps: steps:
- uses: rlespinasse/github-slug-action@v3.x - uses: rlespinasse/github-slug-action@v3.x
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
token: ${{ secrets.API_TOKEN_GITHUB }} fetch-depth: 0
- name: Install dependencies - uses: trilom/file-changes-action@v1.2.4
run: | id: file_changes
apt-get update && apt-get install -y dos2unix
- name: Format files
run: |
bin/qmk cformat -a
bin/qmk pyformat
bin/qmk fileformat
- name: Become QMK Bot
run: |
git config user.name 'QMK Bot'
git config user.email 'hello@qmk.fm'
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with: with:
delete-branch: true output: ' '
branch: bugfix/format_${{ env.GITHUB_REF_SLUG }} fileOutput: ' '
author: QMK Bot <hello@qmk.fm>
committer: QMK Bot <hello@qmk.fm> - name: Run qmk cformat and qmk pyformat
commit-message: Format code according to conventions shell: 'bash {0}'
title: '[CI] Format code according to conventions' run: |
qmk cformat -n $(< ~/files.txt)
cformat_exit=$?
qmk pyformat -n
pyformat_exit=$?
exit $((cformat_exit + pyformat_exit))

View File

@ -1,6 +1,7 @@
"""Format C code according to QMK's style. """Format C code according to QMK's style.
""" """
import subprocess import subprocess
from os import path
from shutil import which from shutil import which
from argcomplete.completers import FilesCompleter from argcomplete.completers import FilesCompleter
@ -9,58 +10,118 @@ from milc import cli
from qmk.path import normpath from qmk.path import normpath
from qmk.c_parse import c_source_files from qmk.c_parse import c_source_files
c_file_suffixes = ('c', 'h', 'cpp')
core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
def cformat_run(files, all_files):
def find_clang_format():
"""Returns the path to clang-format.
"""
for clang_version in range(20, 6, -1):
binary = f'clang-format-{clang_version}'
if which(binary):
return binary
return 'clang-format'
def find_diffs(files):
"""Run clang-format and diff it against a file.
"""
found_diffs = False
for file in files:
cli.log.debug('Checking for changes in %s', file)
clang_format = subprocess.Popen([find_clang_format(), file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
if diff.returncode != 0:
print(diff.stdout)
found_diffs = True
return found_diffs
def cformat_run(files):
"""Spawn clang-format subprocess with proper arguments """Spawn clang-format subprocess with proper arguments
""" """
# Determine which version of clang-format to use # Determine which version of clang-format to use
clang_format = ['clang-format', '-i'] clang_format = [find_clang_format(), '-i']
for clang_version in range(20, 6, -1):
binary = 'clang-format-%d' % clang_version
if which(binary):
clang_format[0] = binary
break
try:
if not files:
cli.log.warn('No changes detected. Use "qmk cformat -a" to format all files')
return False
subprocess.run(clang_format + [file for file in files], check=True)
cli.log.info('Successfully formatted the C code.')
except subprocess.CalledProcessError: try:
cli.run(clang_format + list(map(str, files)), check=True, capture_output=False)
cli.log.info('Successfully formatted the C code.')
return True
except subprocess.CalledProcessError as e:
cli.log.error('Error formatting C code!') cli.log.error('Error formatting C code!')
cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
cli.log.debug('STDOUT:')
cli.log.debug(e.stdout)
cli.log.debug('STDERR:')
cli.log.debug(e.stderr)
return False return False
@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.') def filter_files(files):
"""Yield only files to be formatted and skip the rest
"""
for file in files:
if file.name.split('.')[-1] in c_file_suffixes:
yield file
else:
cli.log.debug('Skipping file %s', file)
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.') @cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
@cli.argument('files', nargs='*', arg_only=True, completer=FilesCompleter('.c'), help='Filename(s) to format.') @cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.')
@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True) @cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
def cformat(cli): def cformat(cli):
"""Format C code according to QMK's style. """Format C code according to QMK's style.
""" """
# Empty array for files
files = []
# Core directories for formatting
core_dirs = ['drivers', 'quantum', 'tests', 'tmk_core', 'platforms']
ignores = ['tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios']
# Find the list of files to format # Find the list of files to format
if cli.args.files: if cli.args.files:
files.extend(normpath(file) for file in cli.args.files) files = list(filter_files(cli.args.files))
if not files:
cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
exit(0)
if cli.args.all_files: if cli.args.all_files:
cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files))) cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
# If -a is specified
elif cli.args.all_files: elif cli.args.all_files:
all_files = c_source_files(core_dirs) all_files = c_source_files(core_dirs)
# The following statement checks each file to see if the file path is in the ignored directories. # The following statement checks each file to see if the file path is in the ignored directories.
files.extend(file for file in all_files if not any(i in str(file) for i in ignores)) files = [file for file in all_files if not any(i in str(file) for i in ignored)]
# No files specified & no -a flag
else: else:
base_args = ['git', 'diff', '--name-only', cli.args.base_branch] git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
out = subprocess.run(base_args + core_dirs, check=True, stdout=subprocess.PIPE) git_diff = cli.run(git_diff_cmd)
changed_files = filter(None, out.stdout.decode('UTF-8').split('\n'))
filtered_files = [normpath(file) for file in changed_files if not any(i in file for i in ignores)] if git_diff.returncode != 0:
files.extend(file for file in filtered_files if file.exists() and file.suffix in ['.c', '.h', '.cpp']) cli.log.error("Error running %s", git_diff_cmd)
print(git_diff.stderr)
return git_diff.returncode
files = []
for file in git_diff.stdout.strip().split('\n'):
if not any([file.startswith(ignore) for ignore in ignored]):
if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
files.append(file)
# Sanity check
if not files:
cli.log.error('No changed files detected. Use "qmk cformat -a" to format all files')
return False
# Run clang-format on the files we've found # Run clang-format on the files we've found
cformat_run(files, cli.args.all_files) if cli.args.dry_run:
return not find_diffs(files)
else:
return cformat_run(files)

View File

@ -13,6 +13,7 @@ from qmk.json_schema import json_load
from qmk.keyboard import list_keyboards from qmk.keyboard import list_keyboards
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't write the data to disk.")
@cli.subcommand('Creates a new keymap for the keyboard of your choosing', hidden=False if cli.config.user.developer else True) @cli.subcommand('Creates a new keymap for the keyboard of your choosing', hidden=False if cli.config.user.developer else True)
def generate_api(cli): def generate_api(cli):
"""Generates the QMK API data. """Generates the QMK API data.
@ -40,10 +41,14 @@ def generate_api(cli):
keyboard_readme_src = Path('keyboards') / keyboard_name / 'readme.md' keyboard_readme_src = Path('keyboards') / keyboard_name / 'readme.md'
keyboard_dir.mkdir(parents=True, exist_ok=True) keyboard_dir.mkdir(parents=True, exist_ok=True)
keyboard_info.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': {keyboard_name: kb_all[keyboard_name]}})) keyboard_json = json.dumps({'last_updated': current_datetime(), 'keyboards': {keyboard_name: kb_all[keyboard_name]}})
if not cli.args.dry_run:
keyboard_info.write_text(keyboard_json)
cli.log.debug('Wrote file %s', keyboard_info)
if keyboard_readme_src.exists(): if keyboard_readme_src.exists():
copyfile(keyboard_readme_src, keyboard_readme) copyfile(keyboard_readme_src, keyboard_readme)
cli.log.debug('Copied %s -> %s', keyboard_readme_src, keyboard_readme)
if 'usb' in kb_all[keyboard_name]: if 'usb' in kb_all[keyboard_name]:
usb = kb_all[keyboard_name]['usb'] usb = kb_all[keyboard_name]['usb']
@ -57,20 +62,26 @@ def generate_api(cli):
if 'vid' in usb and 'pid' in usb: if 'vid' in usb and 'pid' in usb:
usb_list[usb['vid']][usb['pid']][keyboard_name] = usb usb_list[usb['vid']][usb['pid']][keyboard_name] = usb
# Write the global JSON files # Generate data for the global files
keyboard_all_file.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': kb_all}, cls=InfoJSONEncoder))
usb_file.write_text(json.dumps({'last_updated': current_datetime(), 'usb': usb_list}, cls=InfoJSONEncoder))
keyboard_list = sorted(kb_all) keyboard_list = sorted(kb_all)
keyboard_list_file.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': keyboard_list}, cls=InfoJSONEncoder))
keyboard_aliases = json_load(Path('data/mappings/keyboard_aliases.json')) keyboard_aliases = json_load(Path('data/mappings/keyboard_aliases.json'))
keyboard_aliases_file.write_text(json.dumps({'last_updated': current_datetime(), 'keyboard_aliases': keyboard_aliases}, cls=InfoJSONEncoder))
keyboard_metadata = { keyboard_metadata = {
'last_updated': current_datetime(), 'last_updated': current_datetime(),
'keyboards': keyboard_list, 'keyboards': keyboard_list,
'keyboard_aliases': keyboard_aliases, 'keyboard_aliases': keyboard_aliases,
'usb': usb_list, 'usb': usb_list,
} }
keyboard_metadata_file.write_text(json.dumps(keyboard_metadata, cls=InfoJSONEncoder))
# Write the global JSON files
keyboard_all_json = json.dumps({'last_updated': current_datetime(), 'keyboards': kb_all}, cls=InfoJSONEncoder)
usb_json = json.dumps({'last_updated': current_datetime(), 'usb': usb_list}, cls=InfoJSONEncoder)
keyboard_list_json = json.dumps({'last_updated': current_datetime(), 'keyboards': keyboard_list}, cls=InfoJSONEncoder)
keyboard_aliases_json = json.dumps({'last_updated': current_datetime(), 'keyboard_aliases': keyboard_aliases}, cls=InfoJSONEncoder)
keyboard_metadata_json = json.dumps(keyboard_metadata, cls=InfoJSONEncoder)
if not cli.args.dry_run:
keyboard_all_file.write_text(keyboard_all_json)
usb_file.write_text(usb_json)
keyboard_list_file.write_text(keyboard_list_json)
keyboard_aliases_file.write_text(keyboard_aliases_json)
keyboard_metadata_file.write_text(keyboard_metadata_json)

View File

@ -5,13 +5,22 @@ from milc import cli
import subprocess import subprocess
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True) @cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True)
def pyformat(cli): def pyformat(cli):
"""Format python code according to QMK's style. """Format python code according to QMK's style.
""" """
edit = '--diff' if cli.args.dry_run else '--in-place'
yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python']
try: try:
subprocess.run(['yapf', '-vv', '-ri', 'bin/qmk', 'lib/python'], check=True) cli.run(yapf_cmd, check=True, capture_output=False)
cli.log.info('Successfully formatted the python code in `bin/qmk` and `lib/python`.') cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.')
return True
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
cli.log.error('Error formatting python code!') if cli.args.dry_run:
cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!')
else:
cli.log.error('Error formatting python code!')
return False

View File

@ -11,6 +11,7 @@ from milc import cli
def pytest(cli): def pytest(cli):
"""Run several linting/testing commands. """Run several linting/testing commands.
""" """
flake8 = subprocess.run(['flake8', 'lib/python', 'bin/qmk'])
nose2 = subprocess.run(['nose2', '-v']) nose2 = subprocess.run(['nose2', '-v'])
flake8 = subprocess.run(['flake8', 'lib/python', 'bin/qmk'])
return flake8.returncode | nose2.returncode return flake8.returncode | nose2.returncode

View File

@ -33,10 +33,15 @@ def check_returncode(result, expected=[0]):
def test_cformat(): def test_cformat():
result = check_subcommand('cformat', 'quantum/matrix.c') result = check_subcommand('cformat', '-n', 'quantum/matrix.c')
check_returncode(result) check_returncode(result)
def test_cformat_all():
result = check_subcommand('cformat', '-n', '-a')
check_returncode(result, [0, 1])
def test_compile(): def test_compile():
result = check_subcommand('compile', '-kb', 'handwired/pytest/basic', '-km', 'default', '-n') result = check_subcommand('compile', '-kb', 'handwired/pytest/basic', '-km', 'default', '-n')
check_returncode(result) check_returncode(result)
@ -83,9 +88,9 @@ def test_hello():
def test_pyformat(): def test_pyformat():
result = check_subcommand('pyformat') result = check_subcommand('pyformat', '--dry-run')
check_returncode(result) check_returncode(result)
assert 'Successfully formatted the python code' in result.stdout assert 'Python code in `bin/qmk` and `lib/python` is correctly formatted.' in result.stdout
def test_list_keyboards(): def test_list_keyboards():
@ -225,6 +230,11 @@ def test_clean():
assert result.stdout.count('done') == 2 assert result.stdout.count('done') == 2
def test_generate_api():
result = check_subcommand('generate-api', '--dry-run')
check_returncode(result)
def test_generate_rgb_breathe_table(): def test_generate_rgb_breathe_table():
result = check_subcommand("generate-rgb-breathe-table", "-c", "1.2", "-m", "127") result = check_subcommand("generate-rgb-breathe-table", "-c", "1.2", "-m", "127")
check_returncode(result) check_returncode(result)