mirror of
https://gitee.com/dolphinscheduler/DolphinScheduler.git
synced 2024-11-30 03:08:01 +08:00
[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:
parent
f5e7da3cf6
commit
54933b33e3
17
.github/workflows/py-ci.yml
vendored
17
.github/workflows/py-ci.yml
vendored
@ -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
9
.gitignore
vendored
@ -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/
|
||||
|
32
dolphinscheduler-python/pydolphinscheduler/.coveragerc
Normal file
32
dolphinscheduler-python/pydolphinscheduler/.coveragerc
Normal 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]
|
||||
# Don’t 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
|
@ -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
|
||||
|
@ -18,6 +18,8 @@
|
||||
# testting
|
||||
pytest~=6.2.5
|
||||
freezegun
|
||||
# Test coverage
|
||||
coverage
|
||||
# code linting and formatting
|
||||
flake8
|
||||
flake8-docstrings
|
||||
|
@ -94,6 +94,7 @@ class Delimiter(str):
|
||||
BAR = "-"
|
||||
DASH = "/"
|
||||
COLON = ":"
|
||||
UNDERSCORE = "_"
|
||||
|
||||
|
||||
class Time(str):
|
||||
|
@ -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:])
|
||||
|
@ -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 = {
|
||||
|
@ -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])
|
||||
|
@ -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."""
|
||||
|
@ -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}."
|
Loading…
Reference in New Issue
Block a user