Skip to content

Commit 8fe0f6f

Browse files
Add workflow to test code coverage and generate coverage report (#24)
* Add workflow to test code coverage and generate coverage report This commit adds a GitHub Actions workflow (`coverage.yml`) . The workflow runs on pull requests targeting the `main` branch and consists of a job named `test-and-coverage`. The job runs tests with coverage enabled, generates an HTML coverage report, and uploads the report as an artifact. Additionally, the workflow extracts the pull request title and description and comments on the pull request with the coverage results. * Add utransport test cases * Increase code coverage With these new tests, we were able to increase code coverage to 98%. Along with this, we provided some linting improvements, as well as fixed a few methods that were implemented incorrectly in a previous PR. * Update pyproject.toml Current system does not support tool.coverage.report, so I have removed it for now. Those exclusion cases will be covered in the coverage.rc file * Add steps to comment on the PR with the coverage report * Remove comment on PR step --------- Co-authored-by: Matthew D'Alonzo <matthew.dalonzo@gm.com>
1 parent 3a007cf commit 8fe0f6f

17 files changed

Lines changed: 599 additions & 47 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: Python Test and Coverage
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
8+
9+
jobs:
10+
test-and-coverage:
11+
name: Test with coverage
12+
runs-on: ubuntu-latest
13+
permissions:
14+
pull-requests: write
15+
16+
steps:
17+
- run: |
18+
git config --global user.name 'eclipse-uprotocol-bot'
19+
git config --global user.email 'uprotocol-bot@eclipse.org'
20+
21+
- name: Checkout code
22+
uses: actions/checkout@v3
23+
24+
- name: Set up Apache Maven Central
25+
uses: actions/setup-java@v3
26+
with: # configure settings.xml
27+
distribution: 'temurin'
28+
java-version: '11'
29+
server-id: ossrh
30+
server-username: OSSRH_USER
31+
server-password: OSSRH_PASSWORD
32+
33+
- name: Set up Python
34+
uses: actions/setup-python@v3
35+
with:
36+
python-version: '3.x'
37+
38+
- name: Install Poetry
39+
run: |
40+
python -m pip install --upgrade pip
41+
python -m pip install poetry
42+
43+
- name: Install dependencies
44+
run: |
45+
poetry install
46+
47+
- name: Run prebuild script
48+
run: |
49+
cd scripts
50+
# Run the script within the Poetry virtual environment
51+
poetry run python pull_and_compile_protos.py
52+
53+
- name: Run tests with coverage
54+
run: |
55+
poetry run coverage run --source=uprotocol --omit=uprotocol/proto/*,uprotocol/cloudevent/*_pb2.py,tests/*,*/__init__.py -m pytest
56+
poetry run coverage report > coverage_report.txt
57+
export COVERAGE_PERCENTAGE=$(awk '/TOTAL/{print $4}' coverage_report.txt)
58+
echo "COVERAGE_PERCENTAGE=$COVERAGE_PERCENTAGE" >> $GITHUB_ENV
59+
echo "COVERAGE_PERCENTAGE: $COVERAGE_PERCENTAGE"
60+
poetry run coverage html
61+
62+
63+
- name: Upload coverage report
64+
uses: actions/upload-artifact@v2
65+
with:
66+
name: coverage-report
67+
path: htmlcov/
68+
69+
- name: Check code coverage
70+
uses: actions/github-script@v6
71+
with:
72+
script: |
73+
const COVERAGE_PERCENTAGE = process.env.COVERAGE_PERCENTAGE;
74+
if (parseInt(COVERAGE_PERCENTAGE) < 95){
75+
core.setFailed(`Coverage Percentage is less than 95%: ${COVERAGE_PERCENTAGE}`);
76+
}else{
77+
core.info(`Success`);
78+
core.info(parseInt(COVERAGE_PERCENTAGE));
79+
}
80+
81+
82+
# - name: Comment PR with coverage results
83+
# uses: actions/github-script@v6
84+
# with:
85+
# github-token: ${{ secrets.GITHUB_TOKEN }}
86+
#
87+
# script: |
88+
# const COVERAGE_PERCENTAGE = process.env.COVERAGE_PERCENTAGE;;
89+
# const COVERAGE_REPORT_PATH = `https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/`;
90+
# github.rest.issues.createComment({
91+
# issue_number: context.issue.number,
92+
# owner: context.repo.owner,
93+
# repo: context.repo.repo,
94+
# body: `
95+
# Code coverage report is ready! :chart_with_upwards_trend:
96+
#
97+
# - **Code Coverage Percentage:** ${COVERAGE_PERCENTAGE}
98+
# - **Code Coverage Report:** [View Coverage Report](${COVERAGE_REPORT_PATH})
99+
# `
100+
# });
101+
#

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
.coverage
99
target
1010
#**/proto
11-
poetry.lock
11+
poetry.lock
12+
htmlcov
13+
coverage_report.txt

README.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,4 @@ Clean up by running the command:
112112
Requires coverage to be installed first, that can be done by running `pip install coverage`
113113

114114
then you run:
115-
`python -m coverage run --source tests/ -m unittest discover`
115+
`python -m coverage run --source tests/ -m unittest discover`

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ gitpython = ">=3.1.41"
2020
googleapis-common-protos = ">=1.56.4"
2121
protobuf = "4.24.2"
2222

23+
[tool.poetry.dev-dependencies]
24+
pytest = "^6.2"
25+
coverage = "^5.5"
26+
2327
[build-system]
2428
requires = ["poetry-core"]
2529
build-backend = "poetry.core.masonry.api"

tests/test_cloudevent/test_datamodel/test_ucloudevent.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@
2424
#
2525
# -------------------------------------------------------------------------
2626

27+
from datetime import datetime, timezone, timedelta
28+
2729
from uprotocol.proto.uri_pb2 import UUri, UEntity, UResource
2830
from uprotocol.uri.serializer.longuriserializer import LongUriSerializer
2931
from uprotocol.cloudevent.cloudevents_pb2 import CloudEvent
3032
from uprotocol.proto.uattributes_pb2 import UMessageType, UPriority
33+
from uprotocol.proto.upayload_pb2 import UPayloadFormat
3134
from uprotocol.cloudevent.factory.cloudeventfactory import CloudEventFactory
3235
from uprotocol.proto.ustatus_pb2 import UCode
3336
from uprotocol.uuid.factory.uuidfactory import Factories
@@ -90,6 +93,31 @@ def build_cloud_event_for_test():
9093
return cloud_event
9194

9295

96+
def build_cloud_event_for_test_with_id(id):
97+
source = build_uri_for_test()
98+
proto_payload = build_proto_payload_for_test()
99+
# additional attributes
100+
u_cloud_event_attributes = (
101+
UCloudEventAttributesBuilder()
102+
.with_hash("somehash")
103+
.with_priority(UPriority.UPRIORITY_CS1)
104+
.with_ttl(3)
105+
.with_token("someOAuthToken")
106+
.build()
107+
)
108+
109+
# build the cloud event
110+
cloud_event = CloudEventFactory.build_base_cloud_event(
111+
id,
112+
source,
113+
proto_payload.SerializeToString(),
114+
proto_payload.type_url,
115+
u_cloud_event_attributes,
116+
UCloudEvent.get_event_type(UMessageType.UMESSAGE_TYPE_PUBLISH),
117+
)
118+
return cloud_event
119+
120+
93121
class TestUCloudEvent(unittest.TestCase):
94122
DATA_CONTENT_TYPE = "application/x-protobuf"
95123

@@ -102,6 +130,7 @@ def test_extract_sink_from_cloudevent_when_sink_exists(self):
102130
sink = "//bo.cloud/petapp/1/rpc.response"
103131
cloud_event = build_cloud_event_for_test()
104132
cloud_event.__setitem__("sink", sink)
133+
cloud_event.__setitem__("plevel", 4)
105134
self.assertEqual(sink, UCloudEvent.get_sink(cloud_event))
106135

107136
def test_extract_sink_from_cloudevent_when_sink_does_not_exist(self):
@@ -193,6 +222,82 @@ def test_extract_platform_error_from_cloudevent_when_platform_error_exists_in_wr
193222
UCode.OK, UCloudEvent.get_communication_status(cloud_event)
194223
)
195224

225+
def test_extract_platform_error_from_cloudevent_when_platform_error_exists(
226+
self,
227+
):
228+
cloud_event = build_cloud_event_for_test()
229+
cloud_event.__setitem__("commstatus", UCode.INVALID_ARGUMENT)
230+
self.assertEqual(
231+
UCode.INVALID_ARGUMENT,
232+
UCloudEvent.get_communication_status(cloud_event),
233+
)
234+
235+
def test_extract_platform_error_from_cloudevent_when_platform_error_does_not_exist(
236+
self,
237+
):
238+
cloud_event = build_cloud_event_for_test()
239+
self.assertEqual(
240+
UCode.OK, UCloudEvent.get_communication_status(cloud_event)
241+
)
242+
243+
def test_adding_platform_error_to_existing_cloudevent(
244+
self,
245+
):
246+
cloud_event = build_cloud_event_for_test()
247+
self.assertEqual(
248+
UCode.OK, UCloudEvent.get_communication_status(cloud_event)
249+
)
250+
251+
cloud_event_1 = UCloudEvent.add_communication_status(
252+
cloud_event, UCode.DEADLINE_EXCEEDED
253+
)
254+
255+
self.assertEqual(
256+
UCode.OK, UCloudEvent.get_communication_status(cloud_event)
257+
)
258+
259+
self.assertEqual(
260+
UCode.DEADLINE_EXCEEDED,
261+
UCloudEvent.get_communication_status(cloud_event_1),
262+
)
263+
264+
def test_adding_empty_platform_error_to_existing_cloudevent(
265+
self,
266+
):
267+
cloud_event = build_cloud_event_for_test()
268+
self.assertEqual(
269+
UCode.OK, UCloudEvent.get_communication_status(cloud_event)
270+
)
271+
272+
cloud_event_1 = UCloudEvent.add_communication_status(cloud_event, None)
273+
274+
self.assertEqual(
275+
UCode.OK, UCloudEvent.get_communication_status(cloud_event)
276+
)
277+
278+
self.assertEqual(cloud_event, cloud_event_1)
279+
280+
def test_extract_creation_timestamp_from_cloudevent_UUID_Id_when_not_a_UUIDV8_id(
281+
self,
282+
):
283+
cloud_event = build_cloud_event_for_test()
284+
self.assertEqual(None, UCloudEvent.get_creation_timestamp(cloud_event))
285+
286+
def test_extract_creation_timestamp_from_cloudevent_UUIDV8_Id_when_UUIDV8_id_is_valid(
287+
self,
288+
):
289+
uuid = Factories.UPROTOCOL.create()
290+
str_uuid = LongUuidSerializer.instance().serialize(uuid)
291+
cloud_event = build_cloud_event_for_test_with_id(str_uuid)
292+
maybe_creation_timestamp = UCloudEvent.get_creation_timestamp(
293+
cloud_event
294+
)
295+
self.assertIsNotNone(maybe_creation_timestamp)
296+
creation_timestamp = maybe_creation_timestamp / 1000
297+
298+
now_timestamp = datetime.now(timezone.utc).timestamp()
299+
self.assertAlmostEqual(creation_timestamp, now_timestamp, delta=1)
300+
196301
def test_cloudevent_is_not_expired_cd_when_no_ttl_configured(self):
197302
cloud_event = build_cloud_event_for_test()
198303
cloud_event.__delitem__("ttl")
@@ -214,6 +319,14 @@ def test_cloudevent_is_not_expired_cd_when_ttl_is_minus_one(self):
214319
UCloudEvent.is_expired_by_cloud_event_creation_date(cloud_event)
215320
)
216321

322+
def test_cloudevent_is_expired_cd_when_ttl_is_one(self):
323+
cloud_event = build_cloud_event_for_test()
324+
cloud_event.__setitem__("ttl", 1)
325+
time.sleep(0.002)
326+
self.assertTrue(
327+
UCloudEvent.is_expired_by_cloud_event_creation_date(cloud_event)
328+
)
329+
217330
def test_cloudevent_is_expired_when_ttl_1_mili(self):
218331
uuid = Factories.UPROTOCOL.create()
219332
str_uuid = LongUuidSerializer.instance().serialize(uuid)
@@ -294,6 +407,8 @@ def test_from_message_with_valid_message(self):
294407
UCloudEvent.get_type(cloud_event1),
295408
)
296409

410+
411+
297412
def test_to_from_message_from_request_cloudevent(self):
298413
# additional attributes
299414
u_cloud_event_attributes = (
@@ -444,6 +559,8 @@ def test_to_from_message_from_UCP_cloudevent(self):
444559
u_cloud_event_attributes,
445560
)
446561
cloud_event.__setitem__("priority", "CS4")
562+
cloud_event.__setitem__("commstatus", 16)
563+
cloud_event.__setitem__("permission_level", 4)
447564

448565
result = UCloudEvent.toMessage(cloud_event)
449566
self.assertIsNotNone(result)
@@ -458,3 +575,39 @@ def test_from_message_with_null_message(self):
458575
with self.assertRaises(ValueError) as context:
459576
UCloudEvent.fromMessage(None)
460577
self.assertTrue("message cannot be null." in context.exception)
578+
579+
def test_cloud_event_to_string(self):
580+
u_cloud_event_attributes = (
581+
UCloudEventAttributesBuilder()
582+
.with_ttl(3)
583+
.with_token("someOAuthToken")
584+
.build()
585+
)
586+
587+
cloud_event = CloudEventFactory.request(
588+
build_uri_for_test(),
589+
"//bo.cloud/petapp/1/rpc.response",
590+
CloudEventFactory.generate_cloud_event_id(),
591+
build_proto_payload_for_test(),
592+
u_cloud_event_attributes,
593+
)
594+
cloud_event_string = UCloudEvent.to_string(cloud_event)
595+
self.assertTrue(
596+
"source='/body.access//door.front_left#Door', sink='//bo.cloud/petapp/1/rpc.response', type='req.v1'}"
597+
in cloud_event_string
598+
)
599+
600+
def test_cloud_event_to_string_none(self):
601+
cloud_event_string = UCloudEvent.to_string(None)
602+
self.assertEqual(
603+
cloud_event_string, "null"
604+
)
605+
606+
def test_get_upayload_format_from_content_type(self):
607+
new_format = UCloudEvent().get_upayload_format_from_content_type("application/json")
608+
self.assertEqual(new_format, UPayloadFormat.UPAYLOAD_FORMAT_JSON)
609+
610+
def test_to_message_none_entry(self):
611+
with self.assertRaises(ValueError) as context:
612+
UCloudEvent().toMessage(None)
613+
self.assertTrue("Cloud Event can't be None" in context.exception)

tests/test_cloudevent/test_datamodel/test_ucloudeventattributes.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,29 @@ def test_is_empty_function_permutations(self):
9898
u_cloud_event_attributes5 = UCloudEventAttributesBuilder().with_ttl(8).build()
9999
self.assertFalse(u_cloud_event_attributes5.is_empty())
100100

101+
def test__eq__is_same(self):
102+
u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash(" ").with_token(" ").build()
103+
self.assertTrue(u_cloud_event_attributes.__eq__(u_cloud_event_attributes))
104+
105+
def test__eq__is_equal(self):
106+
u_cloud_event_attributes_1 = UCloudEventAttributesBuilder().with_hash(" ").with_token(" ").build()
107+
u_cloud_event_attributes_2 = UCloudEventAttributesBuilder().with_hash(" ").with_token(" ").build()
108+
self.assertTrue(u_cloud_event_attributes_1.__eq__(u_cloud_event_attributes_2))
109+
110+
def test__eq__is_not_equal(self):
111+
u_cloud_event_attributes_1 = UCloudEventAttributesBuilder().with_hash(" ").with_token(" ").build()
112+
u_cloud_event_attributes_2 = UCloudEventAttributesBuilder().with_hash(" ").with_token("12345").build()
113+
self.assertFalse(u_cloud_event_attributes_1.__eq__(u_cloud_event_attributes_2))
114+
115+
def test__hash__same(self):
116+
u_cloud_event_attributes_1 = UCloudEventAttributesBuilder().with_hash(" ").with_token(" ").build()
117+
self.assertEqual(hash(u_cloud_event_attributes_1), hash(u_cloud_event_attributes_1))
118+
119+
def test__hash__different(self):
120+
u_cloud_event_attributes_1 = UCloudEventAttributesBuilder().with_hash(" ").with_token(" ").build()
121+
u_cloud_event_attributes_2 = UCloudEventAttributesBuilder().with_hash(" ").with_token("12345").build()
122+
self.assertNotEqual(hash(u_cloud_event_attributes_1), hash(u_cloud_event_attributes_2))
123+
101124

102125
if __name__ == '__main__':
103126
unittest.main()

tests/test_cloudevent/test_validator/test_cloudeventvalidator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,4 +640,4 @@ def fetching_the_notification_validator(self):
640640
validator = CloudEventValidator.get_validator(cloud_event)
641641
status = validator.validate_type(cloud_event).to_status()
642642
self.assertEqual(status, ValidationResult.STATUS_SUCCESS)
643-
self.assertEqual("CloudEventValidator.Notification", str(validator))
643+
self.assertEqual("CloudEventValidator.Notification", str(validator))

0 commit comments

Comments
 (0)