[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
run: |
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/test
# ------------------
# pydolphinscheduler
# ------------------
# Cache
__pycache__/
# Build
build/
*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
```
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 -->
[pypi]: https://pypi.org/
[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
[flake8]: https://flake8.pycqa.org/en/latest/index.html
[black-editor]: https://black.readthedocs.io/en/stable/integrations/editors.html#pycharm-intellij-idea
[coverage]: https://coverage.readthedocs.io/en/stable/
<!-- badge -->
[ga-py-test]: https://github.com/apache/dolphinscheduler/actions/workflows/py-ci.yml/badge.svg?branch=dev
[ga]: https://github.com/apache/dolphinscheduler/actions

View File

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

View File

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

View File

@ -17,20 +17,23 @@
"""String util function collections."""
from pydolphinscheduler.constants import Delimiter
def attr2camel(attr: str, include_private=True):
"""Covert class attribute name to camel case."""
if include_private:
attr = attr.lstrip("_")
attr = attr.lstrip(Delimiter.UNDERSCORE)
return snake2camel(attr)
def snake2camel(snake: str):
"""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:])
def class_name2camel(class_name: str):
"""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."""
from datetime import datetime
from typing import Any
from pydolphinscheduler.utils.date import conv_to_schedule
import pytest
@ -135,6 +137,21 @@ def test__parse_datetime(val, expect):
), 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():
"""Test process definition function to_dict without task."""
expect = {

View File

@ -18,8 +18,10 @@
"""Test Task class function."""
from unittest.mock import patch
import pytest
from pydolphinscheduler.core.task import TaskParams, TaskRelation, Task
from tests.testing.task import Task as testTask
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))
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(
"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:
"""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}."