Skip to content
Open
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
54 changes: 54 additions & 0 deletions test_upload_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,60 @@ def test_telemetry_failure_does_not_affect_action_exit_code(self):

self.assertEqual(0, exit_code)

@mock.patch("upload_coverage.threading.Thread")
def test_telemetry_thread_daemon_and_timeout_on_success(self, mock_thread_class):
mock_thread_instance = mock.Mock()
mock_thread_class.return_value = mock_thread_instance

opener = mock.Mock(return_value=FakeResponse())
status_opener = mock.Mock(return_value=FakeResponse())
stdout = io.StringIO()

with redirect_stdout(stdout):
upload_coverage.main(
environ=self.base_env, opener=opener, status_opener=status_opener,
)

# Verify thread constructor arguments
mock_thread_class.assert_called_once()
kwargs = mock_thread_class.call_args.kwargs
self.assertEqual("telemetry-starting", kwargs.get("name"))
self.assertTrue(kwargs.get("daemon"))

# Verify join was called with the configured timeout
from status_report import STATUS_TIMEOUT_SECONDS
mock_thread_instance.join.assert_called_once_with(timeout=STATUS_TIMEOUT_SECONDS)

@mock.patch("upload_coverage.threading.Thread")
def test_telemetry_thread_timeout_on_missing_file(self, mock_thread_class):
mock_thread_instance = mock.Mock()
mock_thread_class.return_value = mock_thread_instance

env = dict(self.base_env, INPUT_FILE="/nonexistent")
status_opener = mock.Mock(return_value=FakeResponse())
stdout = io.StringIO()

with redirect_stdout(stdout):
upload_coverage.main(environ=env, opener=mock.Mock(), status_opener=status_opener)

from status_report import STATUS_TIMEOUT_SECONDS
mock_thread_instance.join.assert_called_once_with(timeout=STATUS_TIMEOUT_SECONDS)

@mock.patch("upload_coverage.threading.Thread")
def test_telemetry_thread_timeout_on_invalid_input(self, mock_thread_class):
mock_thread_instance = mock.Mock()
mock_thread_class.return_value = mock_thread_instance

env = dict(self.base_env, INPUT_LANGUAGE="", REF="", PR_NUMBER="") # will trigger ValueError in build_payload
status_opener = mock.Mock(return_value=FakeResponse())
stdout = io.StringIO()

with redirect_stdout(stdout):
upload_coverage.main(environ=env, opener=mock.Mock(), status_opener=status_opener)

from status_report import STATUS_TIMEOUT_SECONDS
mock_thread_instance.join.assert_called_once_with(timeout=STATUS_TIMEOUT_SECONDS)


if __name__ == "__main__":
unittest.main()
25 changes: 18 additions & 7 deletions upload_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import os
import sys
import threading
import time
import urllib.error
import urllib.request
Expand Down Expand Up @@ -174,23 +175,31 @@ def main(
api_url = env.get("GITHUB_API_URL", "https://api.github.com")
token = env.get("GH_TOKEN", "")

# Send "starting" telemetry report
# Send "starting" telemetry report in a background thread
starting_report = status_report.build_starting_report(env)
status_report.save_state("started_at", starting_report.get("started_at", ""))
status_report.save_state("starting_report", json.dumps(starting_report))
status_report.send_status_report(
starting_report,
repository=repository,
api_url=api_url,
token=token,
opener=status_opener,

telemetry_thread = threading.Thread(
target=status_report.send_status_report,
args=(starting_report,),
kwargs={
"repository": repository,
"api_url": api_url,
"token": token,
"opener": status_opener,
},
name="telemetry-starting",
daemon=True,
)
telemetry_thread.start()

upload_start = time.monotonic()

file_path = env.get("INPUT_FILE", "")
if not file_path or not Path(file_path).is_file():
emit_annotation("error", f"Coverage file not found: {file_path}")
telemetry_thread.join(timeout=status_report.STATUS_TIMEOUT_SECONDS)
_send_completed_report(
starting_report, "user-error",
error_type="file_not_found", error_message=f"Coverage file not found: {file_path}",
Expand Down Expand Up @@ -226,6 +235,7 @@ def main(
)
except ValueError as error:
emit_annotation("error", str(error))
telemetry_thread.join(timeout=status_report.STATUS_TIMEOUT_SECONDS)
_send_completed_report(
starting_report, "user-error",
error_type="invalid_input", error_message=str(error),
Expand Down Expand Up @@ -261,6 +271,7 @@ def main(
error_type = f"http_{http_status}" if http_status else "network_error"
error_message = _extract_message(body)

telemetry_thread.join(timeout=status_report.STATUS_TIMEOUT_SECONDS)
_send_completed_report(
starting_report, telemetry_status,
upload_duration_ms=upload_duration_ms,
Expand Down