[ci][python] Add coverage check in CI (#6861)

* [ci] Add coverage check in CI

* Coverage add dependent

* Install pydolphinscheduler before run coverage

* Up test coverage to 87% and down threshold to 85%

* Fix code style

* Add doc about coverage
This commit is contained in:
Jiajie Zhong 2021-11-17 09:46:40 +08:00 committed by GitHub
parent f5e7da3cf6
commit 54933b33e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 266 additions and 4 deletions

View File

@ -78,3 +78,20 @@ jobs:
- name: Run tests - name: Run tests
run: | run: |
pytest pytest
coverage:
name: Tests coverage
needs:
- pytest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Install Development Dependences
run: |
pip install -r requirements_dev.txt
pip install -e .
- name: Run Tests && Check coverage
run: coverage run && coverage report

9
.gitignore vendored
View File

@ -49,7 +49,16 @@ docker/build/apache-dolphinscheduler*
dolphinscheduler-common/sql dolphinscheduler-common/sql
dolphinscheduler-common/test dolphinscheduler-common/test
# ------------------
# pydolphinscheduler # pydolphinscheduler
# ------------------
# Cache
__pycache__/ __pycache__/
# Build
build/ build/
*egg-info/ *egg-info/
# Test coverage
.coverage
htmlcov/

View File

@ -0,0 +1,32 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
[run]
command_line = -m pytest
omit =
# Ignore all test cases in tests/
tests/*
# TODO. Temporary ignore java_gateway file, because we could not find good way to test it.
src/pydolphinscheduler/java_gateway.py
[report]
# Dont report files that are 100% covered
skip_covered = True
show_missing = True
precision = 2
# Report will fail when coverage under 90.00%
fail_under = 85

View File

@ -132,6 +132,19 @@ To test locally, you could directly run pytest after set `PYTHONPATH`
PYTHONPATH=src/ pytest PYTHONPATH=src/ pytest
``` ```
We try to keep pydolphinscheduler usable through unit test coverage. 90% test coverage is our target, but for
now, we require test coverage up to 85%, and each pull request leas than 85% would fail our CI step
`Tests coverage`. We use [coverage][coverage] to check our test coverage, and you could check it locally by
run command.
```shell
coverage run && coverage report
```
It would not only run unit test but also show each file coverage which cover rate less than 100%, and `TOTAL`
line show you total coverage of you code. If your CI failed with coverage you could go and find some reason by
this command output.
<!-- content --> <!-- content -->
[pypi]: https://pypi.org/ [pypi]: https://pypi.org/
[dev-setup]: https://dolphinscheduler.apache.org/en-us/development/development-environment-setup.html [dev-setup]: https://dolphinscheduler.apache.org/en-us/development/development-environment-setup.html
@ -144,6 +157,7 @@ PYTHONPATH=src/ pytest
[black]: https://black.readthedocs.io/en/stable/index.html [black]: https://black.readthedocs.io/en/stable/index.html
[flake8]: https://flake8.pycqa.org/en/latest/index.html [flake8]: https://flake8.pycqa.org/en/latest/index.html
[black-editor]: https://black.readthedocs.io/en/stable/integrations/editors.html#pycharm-intellij-idea [black-editor]: https://black.readthedocs.io/en/stable/integrations/editors.html#pycharm-intellij-idea
[coverage]: https://coverage.readthedocs.io/en/stable/
<!-- badge --> <!-- badge -->
[ga-py-test]: https://github.com/apache/dolphinscheduler/actions/workflows/py-ci.yml/badge.svg?branch=dev [ga-py-test]: https://github.com/apache/dolphinscheduler/actions/workflows/py-ci.yml/badge.svg?branch=dev
[ga]: https://github.com/apache/dolphinscheduler/actions [ga]: https://github.com/apache/dolphinscheduler/actions

View File

@ -18,6 +18,8 @@
# testting # testting
pytest~=6.2.5 pytest~=6.2.5
freezegun freezegun
# Test coverage
coverage
# code linting and formatting # code linting and formatting
flake8 flake8
flake8-docstrings flake8-docstrings

View File

@ -94,6 +94,7 @@ class Delimiter(str):
BAR = "-" BAR = "-"
DASH = "/" DASH = "/"
COLON = ":" COLON = ":"
UNDERSCORE = "_"
class Time(str): class Time(str):

View File

@ -17,20 +17,23 @@
"""String util function collections.""" """String util function collections."""
from pydolphinscheduler.constants import Delimiter
def attr2camel(attr: str, include_private=True): def attr2camel(attr: str, include_private=True):
"""Covert class attribute name to camel case.""" """Covert class attribute name to camel case."""
if include_private: if include_private:
attr = attr.lstrip("_") attr = attr.lstrip(Delimiter.UNDERSCORE)
return snake2camel(attr) return snake2camel(attr)
def snake2camel(snake: str): def snake2camel(snake: str):
"""Covert snake case to camel case.""" """Covert snake case to camel case."""
components = snake.split("_") components = snake.split(Delimiter.UNDERSCORE)
return components[0] + "".join(x.title() for x in components[1:]) return components[0] + "".join(x.title() for x in components[1:])
def class_name2camel(class_name: str): def class_name2camel(class_name: str):
"""Covert class name string to camel case.""" """Covert class name string to camel case."""
return class_name[0].lower() + class_name[1:] class_name = class_name.lstrip(Delimiter.UNDERSCORE)
return class_name[0].lower() + snake2camel(class_name[1:])

View File

@ -18,6 +18,8 @@
"""Test process definition.""" """Test process definition."""
from datetime import datetime from datetime import datetime
from typing import Any
from pydolphinscheduler.utils.date import conv_to_schedule from pydolphinscheduler.utils.date import conv_to_schedule
import pytest import pytest
@ -135,6 +137,21 @@ def test__parse_datetime(val, expect):
), f"Function _parse_datetime with unexpect value by {val}." ), f"Function _parse_datetime with unexpect value by {val}."
@pytest.mark.parametrize(
"val",
[
20210101,
(2021, 1, 1),
{"year": "2021", "month": "1", "day": 1},
],
)
def test__parse_datetime_not_support_type(val: Any):
"""Test process definition function _parse_datetime not support type error."""
with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME) as pd:
with pytest.raises(ValueError):
pd._parse_datetime(val)
def test_process_definition_to_dict_without_task(): def test_process_definition_to_dict_without_task():
"""Test process definition function to_dict without task.""" """Test process definition function to_dict without task."""
expect = { expect = {

View File

@ -18,8 +18,10 @@
"""Test Task class function.""" """Test Task class function."""
from unittest.mock import patch from unittest.mock import patch
import pytest
from pydolphinscheduler.core.task import TaskParams, TaskRelation, Task from pydolphinscheduler.core.task import TaskParams, TaskRelation, Task
from tests.testing.task import Task as testTask
def test_task_params_to_dict(): def test_task_params_to_dict():
@ -93,3 +95,75 @@ def test_task_to_dict():
): ):
task = Task(name=name, task_type=task_type, task_params=TaskParams(raw_script)) task = Task(name=name, task_type=task_type, task_params=TaskParams(raw_script))
assert task.to_dict() == expect assert task.to_dict() == expect
@pytest.mark.parametrize("shift", ["<<", ">>"])
def test_two_tasks_shift(shift: str):
"""Test bit operator between tasks.
Here we test both `>>` and `<<` bit operator.
"""
raw_script = "script"
upstream = testTask(
name="upstream", task_type=shift, task_params=TaskParams(raw_script)
)
downstream = testTask(
name="downstream", task_type=shift, task_params=TaskParams(raw_script)
)
if shift == "<<":
downstream << upstream
elif shift == ">>":
upstream >> downstream
else:
assert False, f"Unexpect bit operator type {shift}."
assert (
1 == len(upstream._downstream_task_codes)
and downstream.code in upstream._downstream_task_codes
), "Task downstream task attributes error, downstream codes size or specific code failed."
assert (
1 == len(downstream._upstream_task_codes)
and upstream.code in downstream._upstream_task_codes
), "Task upstream task attributes error, upstream codes size or upstream code failed."
@pytest.mark.parametrize(
"dep_expr, flag",
[
("task << tasks", "upstream"),
("tasks << task", "downstream"),
("task >> tasks", "downstream"),
("tasks >> task", "upstream"),
],
)
def test_tasks_list_shift(dep_expr: str, flag: str):
"""Test bit operator between task and sequence of tasks.
Here we test both `>>` and `<<` bit operator.
"""
reverse_dict = {
"upstream": "downstream",
"downstream": "upstream",
}
task_type = "dep_task_and_tasks"
raw_script = "script"
task = testTask(
name="upstream", task_type=task_type, task_params=TaskParams(raw_script)
)
tasks = [
testTask(
name="downstream1", task_type=task_type, task_params=TaskParams(raw_script)
),
testTask(
name="downstream2", task_type=task_type, task_params=TaskParams(raw_script)
),
]
# Use build-in function eval to simply test case and reduce duplicate code
eval(dep_expr)
direction_attr = f"_{flag}_task_codes"
reverse_direction_attr = f"_{reverse_dict[flag]}_task_codes"
assert 2 == len(getattr(task, direction_attr))
assert [t.code in getattr(task, direction_attr) for t in tasks]
assert all([1 == len(getattr(t, reverse_direction_attr)) for t in tasks])
assert all([task.code in getattr(t, reverse_direction_attr) for t in tasks])

View File

@ -63,7 +63,14 @@ def test_conv_from_str_success(src: str, expect: datetime) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"src", ["2021-01-01 010101", "2021:01:01", "202111", "20210101010101"] "src",
[
"2021-01-01 010101",
"2021:01:01",
"202111",
"20210101010101",
"2021:01:01 01:01:01",
],
) )
def test_conv_from_str_not_impl(src: str) -> None: def test_conv_from_str_not_impl(src: str) -> None:
"""Test function conv_from_str fail case.""" """Test function conv_from_str fail case."""

View File

@ -0,0 +1,86 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Test utils.string module."""
from pydolphinscheduler.utils.string import attr2camel, snake2camel, class_name2camel
import pytest
@pytest.mark.parametrize(
"snake, expect",
[
("snake_case", "snakeCase"),
("snake_123case", "snake123Case"),
("snake_c_a_s_e", "snakeCASE"),
("snake__case", "snakeCase"),
("snake_case_case", "snakeCaseCase"),
("_snake_case", "SnakeCase"),
("__snake_case", "SnakeCase"),
("Snake_case", "SnakeCase"),
],
)
def test_snake2camel(snake: str, expect: str):
"""Test function snake2camel, this is a base function for utils.string."""
assert expect == snake2camel(
snake
), f"Test case {snake} do no return expect result {expect}."
@pytest.mark.parametrize(
"attr, expects",
[
# source attribute, (true expect, false expect),
("snake_case", ("snakeCase", "snakeCase")),
("snake_123case", ("snake123Case", "snake123Case")),
("snake_c_a_s_e", ("snakeCASE", "snakeCASE")),
("snake__case", ("snakeCase", "snakeCase")),
("snake_case_case", ("snakeCaseCase", "snakeCaseCase")),
("_snake_case", ("snakeCase", "SnakeCase")),
("__snake_case", ("snakeCase", "SnakeCase")),
("Snake_case", ("SnakeCase", "SnakeCase")),
],
)
def test_attr2camel(attr: str, expects: tuple):
"""Test function attr2camel."""
for idx, expect in enumerate(expects):
include_private = idx % 2 == 0
assert expect == attr2camel(
attr, include_private
), f"Test case {attr} do no return expect result {expect} when include_private is {include_private}."
@pytest.mark.parametrize(
"class_name, expect",
[
("snake_case", "snakeCase"),
("snake_123case", "snake123Case"),
("snake_c_a_s_e", "snakeCASE"),
("snake__case", "snakeCase"),
("snake_case_case", "snakeCaseCase"),
("_snake_case", "snakeCase"),
("_Snake_case", "snakeCase"),
("__snake_case", "snakeCase"),
("__Snake_case", "snakeCase"),
("Snake_case", "snakeCase"),
],
)
def test_class_name2camel(class_name: str, expect: str):
"""Test function class_name2camel."""
assert expect == class_name2camel(
class_name
), f"Test case {class_name} do no return expect result {expect}."