Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bundle analysis comment threshold #271

Merged
merged 1 commit into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions shared/validation/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,38 @@ def validate_str(self, data: str) -> CoverageCommentRequiredChangesANDGroup:
raise Invalid("Failed to parse required_changes")


class ByteSizeSchemaField(object):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice class! Very well organized, documented, easy to read

"""Converts a possible string with byte extension size into integer with number of bytes.
Acceptable extensions are 'mb', 'kb', 'gb', 'b' and 'bytes' (case insensitive).
Also accepts integers, returning the value itself as the number of bytes.

Example:
100 -> 100
"100b" -> 100
"100 mb" -> 100000000
"12KB" -> 12000
"""

def _validate_str(self, data: str) -> int:
data = data.lower()
regex = re.compile(r"^(\d+)\s*(mb|kb|gb|b|bytes)$")
match = regex.match(data)
if match is None:
raise Invalid(
"Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes"
)
size, extension = match.groups()
extension_multiplier = {"b": 1, "bytes": 1, "kb": 1e3, "mb": 1e6, "gb": 1e9}
return int(size) * extension_multiplier[extension]

def validate(self, data: Any) -> int:
if isinstance(data, int):
return data
if isinstance(data, str):
return self._validate_str(data)
raise Invalid(f"Value should be int or str. Received {type(data).__name__}")


class PercentSchemaField(object):
"""
A field for percentages. Accepts both with and without % symbol.
Expand Down
24 changes: 22 additions & 2 deletions shared/validation/user_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,30 @@
"require_bundle_changes": {
"type": ["boolean", "string"],
"allowed": ["bundle_increase", False, True],
"meta": {
"description": "require_bundle_changes instructs Codecov to only post a PR Comment if the requirements are met",
"options": {
False: {
"type": bool,
"description": "post comment even if there's no change in the bundle size",
"default": True,
},
True: {
"type": bool,
"description": "only post comment if there are changes in bundle size (positive or negative)",
},
"bundle_increase": {
"type": str,
"description": "only post comment if the bundle size increases",
},
},
},
},
"bundle_change_threshold": {
"type": "string",
"regex": r"\d+\s*(mb|kb|gb|b|bytes))",
"coerce": "byte_size",
"meta": {
"description": "Threshold for 'require_bundle_changes'. Notifications will only be triggered if the change is larger than the threshold."
},
},
}

Expand Down
4 changes: 4 additions & 0 deletions shared/validation/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from shared.validation.helpers import (
BranchSchemaField,
ByteSizeSchemaField,
CoverageCommentRequirementSchemaField,
CoverageRangeSchemaField,
CustomFixPathSchemaField,
Expand Down Expand Up @@ -41,6 +42,9 @@ def _normalize_coerce_branch_normalize(self, value):
def _normalize_coerce_coverage_comment_required_changes(self, value):
return CoverageCommentRequirementSchemaField().validate(value)

def _normalize_coerce_byte_size(self, value):
return ByteSizeSchemaField().validate(value)

def _validate_comma_separated_strings(self, constraint, field, value):
"""Test the oddity of a value.

Expand Down
65 changes: 65 additions & 0 deletions tests/unit/validation/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

from shared.validation.helpers import (
ByteSizeSchemaField,
CoverageCommentRequirementSchemaField,
CoverageRangeSchemaField,
CustomFixPathSchemaField,
Expand Down Expand Up @@ -486,3 +487,67 @@ def test_coverage_comment_requirement_coercion_fail(self, input, exception_messa
with pytest.raises(Invalid) as exp:
validator.validate(input)
assert exp.value.error_message == exception_message


class TestByteSizeSchemaField(object):

@pytest.mark.parametrize(
"input, expected",
[
(100, 100),
("100b", 100),
("100 mb", 100000000),
("12KB", 12000),
("1 GB", 1000000000),
("12bytes", 12),
("24b", 24),
],
)
def test_byte_size_coercion_success(self, input, expected):
validator = ByteSizeSchemaField()
assert validator.validate(input) == expected

@pytest.mark.parametrize(
"input, error_message",
[
pytest.param(
None, "Value should be int or str. Received NoneType", id="None_input"
),
pytest.param(
[], "Value should be int or str. Received list", id="list_input"
),
pytest.param(
12.34, "Value should be int or str. Received float", id="float_input"
),
pytest.param(
"200",
"Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes",
id="no_extension",
),
pytest.param(
"kb",
"Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes",
id="no_number",
),
pytest.param(
"100kb 100mb",
"Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes",
id="multiple_values",
),
pytest.param(
"200.45mb",
"Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes",
id="float_value_in_str",
),
pytest.param(
"200tb",
"Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes",
id="invalid_extension",
),
],
)
def test_byte_size_coercion_fail(self, input, error_message):
validator = ByteSizeSchemaField()
with pytest.raises(Invalid) as exp:
validator.validate(input)
assert exp.value.error_message == error_message
51 changes: 44 additions & 7 deletions tests/unit/validation/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,50 @@ def test_validate_jacoco_partials(self):
result = validate_yaml(user_input)
assert result == expected_result

@pytest.mark.parametrize(
"input, expected",
[
pytest.param(
{"comment": {"require_bundle_changes": False}},
{"comment": {"require_bundle_changes": False}},
id="no_bundle_changes_required",
),
pytest.param(
{
"comment": {
"require_bundle_changes": True,
"bundle_change_threshold": 1200,
}
},
{
"comment": {
"require_bundle_changes": True,
"bundle_change_threshold": 1200,
}
},
id="bundle_changes_with_threshold",
),
pytest.param(
{
"comment": {
"require_bundle_changes": "bundle_increase",
"bundle_change_threshold": "1mb",
}
},
{
"comment": {
"require_bundle_changes": "bundle_increase",
"bundle_change_threshold": 1000000,
}
},
id="bundle_increase_required_with_threshold",
),
],
)
def test_bundle_analysis_comment_config(self, input, expected):
result = validate_yaml(input)
assert result == expected


class TestValidationConfig(object):
def test_validate_default_config_yaml(self, mocker):
Expand Down Expand Up @@ -859,13 +903,6 @@ def test_validate_default_config_yaml(self, mocker):
assert res == expected_result


@pytest.mark.parametrize(
"input,expected", [pytest.param("10bytes", True, id="bytes_valid")]
)
def test_bundle_change_threshold_regex(input, expected):
pass


def test_validation_with_branches():
user_input = {
"comment": {
Expand Down
Loading