Browse Source

[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
Jiajie Zhong 3 years ago
parent
commit
54933b33e3

+ 17 - 0
.github/workflows/py-ci.yml

@@ -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 - 0
.gitignore

@@ -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 - 0
dolphinscheduler-python/pydolphinscheduler/.coveragerc

@@ -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

+ 14 - 0
dolphinscheduler-python/pydolphinscheduler/README.md

@@ -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

+ 2 - 0
dolphinscheduler-python/pydolphinscheduler/requirements_dev.txt

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

+ 1 - 0
dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/constants.py

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

+ 6 - 3
dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/string.py

@@ -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:])

+ 17 - 0
dolphinscheduler-python/pydolphinscheduler/tests/core/test_process_definition.py

@@ -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 = {

+ 74 - 0
dolphinscheduler-python/pydolphinscheduler/tests/core/test_task.py

@@ -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])

+ 8 - 1
dolphinscheduler-python/pydolphinscheduler/tests/utils/test_date.py

@@ -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."""

+ 86 - 0
dolphinscheduler-python/pydolphinscheduler/tests/utils/test_string.py

@@ -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}."