From 208f93c098a07ffb9e11c4673b2dd626f6d38398 Mon Sep 17 00:00:00 2001 From: omsaisudarshan108 <53947982+omsaisudarshan108@users.noreply.github.com> Date: Sun, 18 Jan 2026 12:19:37 -0500 Subject: [PATCH] Add comprehensive test coverage and tooling improvements - Add extensive test cases for string_utils (convert_to_str, truncate_string) - Add comprehensive test suite for url_utils.url_join function - Add py.typed marker for PEP 561 type hint compliance - Add codecov.yml configuration for coverage tracking Signed-off-by: Claude Co-Authored-By: Claude Opus 4.5 Signed-off-by: omsaisudarshan108 <53947982+omsaisudarshan108@users.noreply.github.com> --- codecov.yml | 56 +++++++++++++++++ src/nat/py.typed | 0 tests/nat/utils/test_string_utils.py | 90 ++++++++++++++++++++++++++++ tests/nat/utils/test_url_utils.py | 72 +++++++++++++++++++++- 4 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 codecov.yml create mode 100644 src/nat/py.typed diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..f3f288ac27 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Codecov configuration for NeMo Agent Toolkit +# See https://docs.codecov.com/docs/codecov-yaml for configuration options + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: + default: + target: auto + threshold: 1% + informational: true + patch: + default: + target: auto + threshold: 1% + informational: true + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,files" + behavior: default + require_changes: true + require_base: false + require_head: true + +ignore: + - "tests/**/*" + - "examples/**/*" + - "docs/**/*" + - "**/__pycache__/**" + - "**/conftest.py" diff --git a/src/nat/py.typed b/src/nat/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/nat/utils/test_string_utils.py b/tests/nat/utils/test_string_utils.py index ee415ebeaf..cdfb7810f9 100644 --- a/tests/nat/utils/test_string_utils.py +++ b/tests/nat/utils/test_string_utils.py @@ -15,9 +15,11 @@ import dataclasses +import pytest from pydantic import BaseModel from nat.utils.string_utils import convert_to_str +from nat.utils.string_utils import truncate_string class _M(BaseModel): @@ -42,3 +44,91 @@ def __str__(self): return f"C({self.x})" assert convert_to_str(C(3)) == "C(3)" + + +def test_convert_to_str_pydantic_model(): + """Test convert_to_str with Pydantic BaseModel.""" + model = _M(a=42, b="test") + result = convert_to_str(model) + assert '"a":42' in result + assert '"b":"test"' in result + + +def test_convert_to_str_pydantic_model_excludes_none(): + """Test that Pydantic model serialization excludes None values.""" + model = _M(a=42) + result = convert_to_str(model) + assert '"a":42' in result + assert '"b"' not in result + + +def test_convert_to_str_empty_list(): + """Test convert_to_str with empty list.""" + assert convert_to_str([]) == "" + + +def test_convert_to_str_empty_dict(): + """Test convert_to_str with empty dictionary.""" + assert convert_to_str({}) == "" + + +def test_convert_to_str_nested_list(): + """Test convert_to_str with nested structures in list.""" + result = convert_to_str([[1, 2], [3, 4]]) + assert "[1, 2]" in result + assert "[3, 4]" in result + + +def test_convert_to_str_numeric_types(): + """Test convert_to_str with various numeric types.""" + assert convert_to_str(42) == "42" + assert convert_to_str(3.14) == "3.14" + assert convert_to_str(True) == "True" + + +class TestTruncateString: + """Tests for truncate_string function.""" + + def test_truncate_none_input(self): + """Test that None input returns None.""" + assert truncate_string(None) is None + + def test_truncate_empty_string(self): + """Test that empty string returns empty string.""" + assert truncate_string("") == "" + + def test_truncate_short_string(self): + """Test that strings shorter than max_length are not truncated.""" + text = "Hello, World!" + assert truncate_string(text, max_length=100) == text + + def test_truncate_exact_length(self): + """Test string with exact max_length is not truncated.""" + text = "x" * 100 + assert truncate_string(text, max_length=100) == text + + def test_truncate_long_string(self): + """Test that long strings are properly truncated with ellipsis.""" + text = "x" * 150 + result = truncate_string(text, max_length=100) + assert len(result) == 100 + assert result.endswith("...") + assert result == "x" * 97 + "..." + + def test_truncate_custom_max_length(self): + """Test truncation with custom max_length.""" + text = "This is a test string" + result = truncate_string(text, max_length=10) + assert len(result) == 10 + assert result == "This is..." + + def test_truncate_very_short_max_length(self): + """Test truncation with very short max_length.""" + text = "Hello" + result = truncate_string(text, max_length=4) + assert result == "H..." + + def test_truncate_preserves_type(self): + """Test that truncate_string preserves string type.""" + result = truncate_string("test", max_length=100) + assert isinstance(result, str) diff --git a/tests/nat/utils/test_url_utils.py b/tests/nat/utils/test_url_utils.py index 9a5cb7dfe7..cae5dc69f3 100644 --- a/tests/nat/utils/test_url_utils.py +++ b/tests/nat/utils/test_url_utils.py @@ -13,9 +13,75 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + from nat.utils.url_utils import url_join -def test_url_join_basic(): - result = url_join("http://example.com", "api", "v1") - assert result == "http://example.com/api/v1" +class TestUrlJoin: + """Tests for url_join function.""" + + def test_url_join_basic(self): + """Test basic URL joining.""" + result = url_join("http://example.com", "api", "v1") + assert result == "http://example.com/api/v1" + + def test_url_join_trailing_slashes(self): + """Test that trailing slashes are properly handled.""" + result = url_join("http://example.com/", "api/", "v1/") + assert result == "http://example.com/api/v1" + + def test_url_join_leading_slashes(self): + """Test that leading slashes are properly stripped.""" + result = url_join("http://example.com", "/api", "/v1") + assert result == "http://example.com/api/v1" + + def test_url_join_mixed_slashes(self): + """Test with mixed leading and trailing slashes.""" + result = url_join("http://example.com/", "/api/", "/v1/") + assert result == "http://example.com/api/v1" + + def test_url_join_single_part(self): + """Test with single URL part.""" + result = url_join("http://example.com") + assert result == "http://example.com" + + def test_url_join_empty_parts(self): + """Test that empty parts result in empty string.""" + result = url_join() + assert result == "" + + def test_url_join_with_query_params(self): + """Test URL joining with query parameters.""" + result = url_join("http://example.com", "api", "v1?key=value") + assert result == "http://example.com/api/v1?key=value" + + def test_url_join_numeric_parts(self): + """Test URL joining with numeric parts.""" + result = url_join("http://example.com", "api", "v1", 123) + assert result == "http://example.com/api/v1/123" + + def test_url_join_with_https(self): + """Test URL joining with HTTPS protocol.""" + result = url_join("https://secure.example.com", "api", "endpoint") + assert result == "https://secure.example.com/api/endpoint" + + def test_url_join_path_only(self): + """Test joining path segments without protocol.""" + result = url_join("api", "v1", "users") + assert result == "api/v1/users" + + def test_url_join_with_port(self): + """Test URL joining with port number.""" + result = url_join("http://example.com:8080", "api", "v1") + assert result == "http://example.com:8080/api/v1" + + def test_url_join_multiple_consecutive_slashes(self): + """Test that multiple consecutive slashes are handled.""" + result = url_join("http://example.com//", "//api//", "//v1//") + assert result == "http://example.com/api/v1" + + def test_url_join_preserves_protocol_slashes(self): + """Test that protocol slashes are properly handled.""" + result = url_join("http://example.com", "path") + assert "http:" in result