From 467441950c4e7a272fccf3d804030adf5b6b13be Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 8 Dec 2025 14:21:37 -0800 Subject: [PATCH 1/9] path open function to convert crlf to lf --- .../samples/agents/assets/.gitattributes | 13 ------ .../tests/samples/test_samples.py | 43 +++++++++++++++++++ 2 files changed, 43 insertions(+), 13 deletions(-) delete mode 100644 sdk/ai/azure-ai-projects/samples/agents/assets/.gitattributes diff --git a/sdk/ai/azure-ai-projects/samples/agents/assets/.gitattributes b/sdk/ai/azure-ai-projects/samples/agents/assets/.gitattributes deleted file mode 100644 index 878545b8b1cb..000000000000 --- a/sdk/ai/azure-ai-projects/samples/agents/assets/.gitattributes +++ /dev/null @@ -1,13 +0,0 @@ -# Force LF line endings for test files to ensure consistent binary representation -# across Windows and Linux platforms. -# -# These files are read and sent as binary REST API request payloads in tests. -# Without consistent line endings, the binary content differs between platforms, -# causing recorded test traffic to be inconsistent and test playback to fail. -# -# By enforcing LF endings, the binary representation remains identical regardless -# of the platform where tests are executed. - -*.md text eol=lf -*.csv text eol=lf -*.jsonl text eol=lf diff --git a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py index 33bd65266702..cc2df802d688 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -5,6 +5,7 @@ # ------------------------------------ import csv, os, pytest, re, inspect, sys import importlib.util +import io import unittest.mock as mock from azure.core.exceptions import HttpResponseError from devtools_testutils.aio import recorded_by_proxy_async @@ -17,6 +18,21 @@ class SampleExecutor: """Helper class for executing sample files with proper environment setup and credential mocking.""" + class ConvertedFileWrapper(io.BufferedReader): + """File wrapper that converts CRLF to LF content while preserving original filename.""" + + def __init__(self, file_path, converted_content): + # Create BytesIO with converted content + self._bytesio = io.BytesIO(converted_content) + # Initialize BufferedReader with the BytesIO + super().__init__(self._bytesio) + # Override name to be the original file path + self._name = file_path + + @property + def name(self): + return self._name + def __init__( self, test_instance: "AzureRecordedTestCase", sample_path: str, env_var_mapping: dict[str, str], **kwargs ): @@ -46,17 +62,43 @@ def __init__( self.module = importlib.util.module_from_spec(spec) self.spec = spec + self._original_open = open def _capture_print(self, *args, **kwargs): """Capture print calls while still outputting to console.""" self.print_calls.append(" ".join(str(arg) for arg in args)) self._original_print(*args, **kwargs) + def _patched_open(self, *args, **kwargs): + """Patch open to convert CRLF to LF for text files.""" + file_path = args[0] if args else kwargs.get("file") + mode = args[1] if len(args) > 1 else kwargs.get("mode", "r") + + # Check if this is binary read mode for text-like files + if "r" in mode and "b" in mode and file_path and isinstance(file_path, str): + # Check file extension to determine if it's a text file + text_extensions = {".txt", ".json", ".jsonl", ".csv", ".md", ".yaml", ".yml", ".xml"} + ext = os.path.splitext(file_path)[1].lower() + if ext in text_extensions: + # Read the original file + with self._original_open(file_path, "rb") as f: + content = f.read() + + # Convert CRLF to LF + converted_content = content.replace(b"\r\n", b"\n") + + # Return wrapped file-like object with converted content + return SampleExecutor.ConvertedFileWrapper(file_path, converted_content) + + return self._original_open(*args, **kwargs) + def execute(self): """Execute a synchronous sample with proper mocking and environment setup.""" + with ( MonkeyPatch.context() as mp, mock.patch("builtins.print", side_effect=self._capture_print), + mock.patch("builtins.open", side_effect=self._patched_open), mock.patch("azure.identity.DefaultAzureCredential") as mock_credential, ): for var_name, var_value in self.env_vars.items(): @@ -77,6 +119,7 @@ async def execute_async(self): with ( MonkeyPatch.context() as mp, mock.patch("builtins.print", side_effect=self._capture_print), + mock.patch("builtins.open", side_effect=self._patched_open), mock.patch("azure.identity.aio.DefaultAzureCredential") as mock_credential, ): for var_name, var_value in self.env_vars.items(): From 42a843bde18ff219c38576a15d568ec662840b65 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 8 Dec 2025 16:46:17 -0800 Subject: [PATCH 2/9] Write to temp file instead --- .../tests/samples/test_samples.py | 46 +-------------- sdk/ai/azure-ai-projects/tests/test_base.py | 59 +++++++++++++++++++ 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py index cc2df802d688..76d01c3385a1 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -5,12 +5,11 @@ # ------------------------------------ import csv, os, pytest, re, inspect, sys import importlib.util -import io import unittest.mock as mock from azure.core.exceptions import HttpResponseError from devtools_testutils.aio import recorded_by_proxy_async from devtools_testutils import AzureRecordedTestCase, recorded_by_proxy, RecordedTransport -from test_base import servicePreparer +from test_base import servicePreparer, patched_open_crlf_to_lf from pytest import MonkeyPatch from azure.ai.projects import AIProjectClient @@ -18,21 +17,6 @@ class SampleExecutor: """Helper class for executing sample files with proper environment setup and credential mocking.""" - class ConvertedFileWrapper(io.BufferedReader): - """File wrapper that converts CRLF to LF content while preserving original filename.""" - - def __init__(self, file_path, converted_content): - # Create BytesIO with converted content - self._bytesio = io.BytesIO(converted_content) - # Initialize BufferedReader with the BytesIO - super().__init__(self._bytesio) - # Override name to be the original file path - self._name = file_path - - @property - def name(self): - return self._name - def __init__( self, test_instance: "AzureRecordedTestCase", sample_path: str, env_var_mapping: dict[str, str], **kwargs ): @@ -62,43 +46,19 @@ def __init__( self.module = importlib.util.module_from_spec(spec) self.spec = spec - self._original_open = open def _capture_print(self, *args, **kwargs): """Capture print calls while still outputting to console.""" self.print_calls.append(" ".join(str(arg) for arg in args)) self._original_print(*args, **kwargs) - def _patched_open(self, *args, **kwargs): - """Patch open to convert CRLF to LF for text files.""" - file_path = args[0] if args else kwargs.get("file") - mode = args[1] if len(args) > 1 else kwargs.get("mode", "r") - - # Check if this is binary read mode for text-like files - if "r" in mode and "b" in mode and file_path and isinstance(file_path, str): - # Check file extension to determine if it's a text file - text_extensions = {".txt", ".json", ".jsonl", ".csv", ".md", ".yaml", ".yml", ".xml"} - ext = os.path.splitext(file_path)[1].lower() - if ext in text_extensions: - # Read the original file - with self._original_open(file_path, "rb") as f: - content = f.read() - - # Convert CRLF to LF - converted_content = content.replace(b"\r\n", b"\n") - - # Return wrapped file-like object with converted content - return SampleExecutor.ConvertedFileWrapper(file_path, converted_content) - - return self._original_open(*args, **kwargs) - def execute(self): """Execute a synchronous sample with proper mocking and environment setup.""" with ( MonkeyPatch.context() as mp, mock.patch("builtins.print", side_effect=self._capture_print), - mock.patch("builtins.open", side_effect=self._patched_open), + mock.patch("builtins.open", side_effect=patched_open_crlf_to_lf), mock.patch("azure.identity.DefaultAzureCredential") as mock_credential, ): for var_name, var_value in self.env_vars.items(): @@ -119,7 +79,7 @@ async def execute_async(self): with ( MonkeyPatch.context() as mp, mock.patch("builtins.print", side_effect=self._capture_print), - mock.patch("builtins.open", side_effect=self._patched_open), + mock.patch("builtins.open", side_effect=patched_open_crlf_to_lf), mock.patch("azure.identity.aio.DefaultAzureCredential") as mock_credential, ): for var_name, var_value in self.env_vars.items(): diff --git a/sdk/ai/azure-ai-projects/tests/test_base.py b/sdk/ai/azure-ai-projects/tests/test_base.py index 1a9a0cd8e5f0..6232b86d282d 100644 --- a/sdk/ai/azure-ai-projects/tests/test_base.py +++ b/sdk/ai/azure-ai-projects/tests/test_base.py @@ -7,6 +7,8 @@ import re import functools import json +import os +import tempfile from typing import Optional, Any, Dict, Final from azure.ai.projects.models import ( Connection, @@ -33,6 +35,9 @@ from azure.ai.projects import AIProjectClient as AIProjectClient from azure.ai.projects.aio import AIProjectClient as AsyncAIProjectClient +# Store reference to built-in open before any mocking occurs +_BUILTIN_OPEN = open + # Load secrets from environment variables servicePreparer = functools.partial( @@ -63,6 +68,60 @@ DEVELOPER_TIER_TRAINING_TYPE: Final[str] = "developerTier" +def patched_open_crlf_to_lf(*args, **kwargs): + """ + Patched open function that converts CRLF to LF for text files. + + This function should be used with mock.patch("builtins.open", side_effect=TestBase.patched_open_crlf_to_lf) + to ensure consistent line endings in test files during recording and playback. + """ + # Extract file path - first positional arg or 'file' keyword arg + if args: + file_path = args[0] + elif "file" in kwargs: + file_path = kwargs["file"] + else: + # No file path provided, just pass through + return _BUILTIN_OPEN(*args, **kwargs) + + # Extract mode - second positional arg or 'mode' keyword arg + if len(args) > 1: + mode = args[1] + else: + mode = kwargs.get("mode", "r") + + # Check if this is binary read mode for text-like files + if "r" in mode and "b" in mode and file_path and isinstance(file_path, str): + # Check file extension to determine if it's a text file + text_extensions = {".txt", ".json", ".jsonl", ".csv", ".md", ".yaml", ".yml", ".xml"} + ext = os.path.splitext(file_path)[1].lower() + if ext in text_extensions: + # Read the original file + with _BUILTIN_OPEN(file_path, "rb") as f: + content = f.read() + + # Convert CRLF to LF + converted_content = content.replace(b"\r\n", b"\n") + + # Only create temp file if conversion was needed + if converted_content != content: + # Create a temporary file with the converted content + temp_fd, temp_path = tempfile.mkstemp(suffix=ext) + os.write(temp_fd, converted_content) + os.close(temp_fd) + # Replace file path with temp path + if args: + # File path was passed as positional arg + return _BUILTIN_OPEN(temp_path, *args[1:], **kwargs) + else: + # File path was passed as keyword arg + kwargs = kwargs.copy() + kwargs["file"] = temp_path + return _BUILTIN_OPEN(**kwargs) + + return _BUILTIN_OPEN(*args, **kwargs) + + class TestBase(AzureRecordedTestCase): test_redteams_params = { From 5dd2f03dd180a765c1f8064959d582d0c744b099 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 8 Dec 2025 19:45:18 -0800 Subject: [PATCH 3/9] save to the same file name --- sdk/ai/azure-ai-projects/tests/test_base.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sdk/ai/azure-ai-projects/tests/test_base.py b/sdk/ai/azure-ai-projects/tests/test_base.py index 6232b86d282d..dabe56c3c337 100644 --- a/sdk/ai/azure-ai-projects/tests/test_base.py +++ b/sdk/ai/azure-ai-projects/tests/test_base.py @@ -105,10 +105,15 @@ def patched_open_crlf_to_lf(*args, **kwargs): # Only create temp file if conversion was needed if converted_content != content: - # Create a temporary file with the converted content - temp_fd, temp_path = tempfile.mkstemp(suffix=ext) - os.write(temp_fd, converted_content) - os.close(temp_fd) + # Create a sub temp folder and save file with same filename + temp_dir = tempfile.mkdtemp() + original_filename = os.path.basename(file_path) + temp_path = os.path.join(temp_dir, original_filename) + + # Write the converted content to the temp file + with _BUILTIN_OPEN(temp_path, "wb") as temp_file: + temp_file.write(converted_content) + # Replace file path with temp path if args: # File path was passed as positional arg From bd42f931a22bf27d815d79891363d1870c4f1535 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 8 Dec 2025 20:10:58 -0800 Subject: [PATCH 4/9] update --- sdk/ai/azure-ai-projects/tests/samples/test_samples.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py index 76d01c3385a1..a60773a0b691 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -171,9 +171,6 @@ def _get_tools_sample_paths(): "sample_agent_memory_search.py", "sample_agent_openapi_with_project_connection.py", "sample_agent_to_agent.py", - "sample_agent_code_interpreter.py", - "sample_agent_file_search.py", - "sample_agent_file_search_in_stream.py", ] samples = [] @@ -196,8 +193,6 @@ def _get_tools_sample_paths_async(): tools_samples_to_skip = [ "sample_agent_mcp_with_project_connection_async.py", "sample_agent_memory_search_async.py", - "sample_agent_code_interpreter_async.py", - "sample_agent_file_search_in_stream_async.py", ] samples = [] From 4a5e8e88997bf1095edc9cc9dff1ec72aefbe209 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 8 Dec 2025 22:28:59 -0800 Subject: [PATCH 5/9] support open_with_lf for test. --- .../agents/tools/test_agent_file_search.py | 4 +- .../tools/test_agent_file_search_async.py | 4 +- .../tools/test_agent_file_search_stream.py | 4 +- .../test_agent_file_search_stream_async.py | 4 +- sdk/ai/azure-ai-projects/tests/test_base.py | 60 ++++++++++++++++++- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search.py b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search.py index ff294cac3af4..8ed3b7cbec33 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search.py +++ b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search.py @@ -8,7 +8,7 @@ import os import pytest from io import BytesIO -from test_base import TestBase, servicePreparer +from test_base import TestBase, servicePreparer, open_with_lf from devtools_testutils import recorded_by_proxy, RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, FileSearchTool @@ -65,7 +65,7 @@ def test_agent_file_search(self, **kwargs): assert vector_store.id # Upload file to vector store - with open(asset_file_path, "rb") as f: + with open_with_lf(asset_file_path, "rb") as f: file = openai_client.vector_stores.files.upload_and_poll( vector_store_id=vector_store.id, file=f, diff --git a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_async.py b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_async.py index 26af2497614f..8ed1edf117cb 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_async.py +++ b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_async.py @@ -7,7 +7,7 @@ import os from io import BytesIO -from test_base import TestBase, servicePreparer +from test_base import TestBase, servicePreparer, open_with_lf from devtools_testutils.aio import recorded_by_proxy_async from devtools_testutils import RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, FileSearchTool @@ -39,7 +39,7 @@ async def test_agent_file_search_async(self, **kwargs): assert vector_store.id # Upload file to vector store - with open(asset_file_path, "rb") as f: + with open_with_lf(asset_file_path, "rb") as f: file = await openai_client.vector_stores.files.upload_and_poll( vector_store_id=vector_store.id, file=f, diff --git a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream.py b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream.py index d7ae5828a0f8..e38a3903f244 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream.py +++ b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream.py @@ -6,7 +6,7 @@ # cSpell:disable import os -from test_base import TestBase, servicePreparer +from test_base import TestBase, servicePreparer, open_with_lf from devtools_testutils import recorded_by_proxy, RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, FileSearchTool @@ -61,7 +61,7 @@ def test_agent_file_search_stream(self, **kwargs): assert vector_store.id # Upload file to vector store - with open(asset_file_path, "rb") as f: + with open_with_lf(asset_file_path, "rb") as f: file = openai_client.vector_stores.files.upload_and_poll( vector_store_id=vector_store.id, file=f, diff --git a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream_async.py b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream_async.py index cd36a65a09f5..94d5c8a36d75 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream_async.py +++ b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream_async.py @@ -6,7 +6,7 @@ # cSpell:disable import os -from test_base import TestBase, servicePreparer +from test_base import TestBase, servicePreparer, open_with_lf from devtools_testutils.aio import recorded_by_proxy_async from devtools_testutils import RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, FileSearchTool @@ -38,7 +38,7 @@ async def test_agent_file_search_stream_async(self, **kwargs): assert vector_store.id # Upload file to vector store - with open(asset_file_path, "rb") as f: + with open_with_lf(asset_file_path, "rb") as f: file = await openai_client.vector_stores.files.upload_and_poll( vector_store_id=vector_store.id, file=f, diff --git a/sdk/ai/azure-ai-projects/tests/test_base.py b/sdk/ai/azure-ai-projects/tests/test_base.py index dabe56c3c337..50130827665f 100644 --- a/sdk/ai/azure-ai-projects/tests/test_base.py +++ b/sdk/ai/azure-ai-projects/tests/test_base.py @@ -9,7 +9,7 @@ import json import os import tempfile -from typing import Optional, Any, Dict, Final +from typing import Optional, Any, Dict, Final, IO, Union, overload, Literal, TextIO, BinaryIO from azure.ai.projects.models import ( Connection, ConnectionType, @@ -39,6 +39,64 @@ _BUILTIN_OPEN = open +@overload +def open_with_lf( + file: Union[str, bytes, os.PathLike, int], + mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"] = "r", + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + closefd: bool = True, + opener: Optional[Any] = None, +) -> TextIO: ... + + +@overload +def open_with_lf( + file: Union[str, bytes, os.PathLike, int], + mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"], + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + closefd: bool = True, + opener: Optional[Any] = None, +) -> BinaryIO: ... + + +@overload +def open_with_lf( + file: Union[str, bytes, os.PathLike, int], + mode: str, + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + closefd: bool = True, + opener: Optional[Any] = None, +) -> IO[Any]: ... + + +def open_with_lf( + file: Union[str, bytes, os.PathLike, int], + mode: str = "r", + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + closefd: bool = True, + opener: Optional[Any] = None, +) -> IO[Any]: + """ + Open function that converts CRLF to LF for text files. + + This function has the same signature as built-in open and converts line endings + to ensure consistent behavior during test recording and playback. + """ + return patched_open_crlf_to_lf(file, mode, buffering, encoding, errors, newline, closefd, opener) + + # Load secrets from environment variables servicePreparer = functools.partial( EnvironmentVariableLoader, From 2bcea68bb7c6a59b8df8f43617a9cfb7c94310ee Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 8 Dec 2025 22:55:29 -0800 Subject: [PATCH 6/9] fix cspell --- sdk/ai/azure-ai-projects/cspell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/cspell.json b/sdk/ai/azure-ai-projects/cspell.json index 890f24167d34..f9f65f043709 100644 --- a/sdk/ai/azure-ai-projects/cspell.json +++ b/sdk/ai/azure-ai-projects/cspell.json @@ -23,7 +23,8 @@ "Ministral", "cogsvc", "evals", - "FineTuning" + "FineTuning", + "closefd" ], "ignorePaths": [ "*.csv", From fd14c58ce4944178dc9c32079036170005e43cd0 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 8 Dec 2025 23:05:07 -0800 Subject: [PATCH 7/9] clean up --- .../agents/tools/test_agent_file_search.py | 4 +- .../tools/test_agent_file_search_async.py | 4 +- .../tools/test_agent_file_search_stream.py | 4 +- .../test_agent_file_search_stream_async.py | 4 +- sdk/ai/azure-ai-projects/tests/test_base.py | 116 +++++++++--------- 5 files changed, 66 insertions(+), 66 deletions(-) diff --git a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search.py b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search.py index 8ed3b7cbec33..b6920ac48d86 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search.py +++ b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search.py @@ -8,7 +8,7 @@ import os import pytest from io import BytesIO -from test_base import TestBase, servicePreparer, open_with_lf +from test_base import TestBase, servicePreparer from devtools_testutils import recorded_by_proxy, RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, FileSearchTool @@ -65,7 +65,7 @@ def test_agent_file_search(self, **kwargs): assert vector_store.id # Upload file to vector store - with open_with_lf(asset_file_path, "rb") as f: + with self.open_with_lf(asset_file_path, "rb") as f: file = openai_client.vector_stores.files.upload_and_poll( vector_store_id=vector_store.id, file=f, diff --git a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_async.py b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_async.py index 8ed1edf117cb..7a8bdc37bd04 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_async.py +++ b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_async.py @@ -7,7 +7,7 @@ import os from io import BytesIO -from test_base import TestBase, servicePreparer, open_with_lf +from test_base import TestBase, servicePreparer from devtools_testutils.aio import recorded_by_proxy_async from devtools_testutils import RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, FileSearchTool @@ -39,7 +39,7 @@ async def test_agent_file_search_async(self, **kwargs): assert vector_store.id # Upload file to vector store - with open_with_lf(asset_file_path, "rb") as f: + with self.open_with_lf(asset_file_path, "rb") as f: file = await openai_client.vector_stores.files.upload_and_poll( vector_store_id=vector_store.id, file=f, diff --git a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream.py b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream.py index e38a3903f244..25a926eaf228 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream.py +++ b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream.py @@ -6,7 +6,7 @@ # cSpell:disable import os -from test_base import TestBase, servicePreparer, open_with_lf +from test_base import TestBase, servicePreparer from devtools_testutils import recorded_by_proxy, RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, FileSearchTool @@ -61,7 +61,7 @@ def test_agent_file_search_stream(self, **kwargs): assert vector_store.id # Upload file to vector store - with open_with_lf(asset_file_path, "rb") as f: + with self.open_with_lf(asset_file_path, "rb") as f: file = openai_client.vector_stores.files.upload_and_poll( vector_store_id=vector_store.id, file=f, diff --git a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream_async.py b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream_async.py index 94d5c8a36d75..518c87c9f44a 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream_async.py +++ b/sdk/ai/azure-ai-projects/tests/agents/tools/test_agent_file_search_stream_async.py @@ -6,7 +6,7 @@ # cSpell:disable import os -from test_base import TestBase, servicePreparer, open_with_lf +from test_base import TestBase, servicePreparer from devtools_testutils.aio import recorded_by_proxy_async from devtools_testutils import RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, FileSearchTool @@ -38,7 +38,7 @@ async def test_agent_file_search_stream_async(self, **kwargs): assert vector_store.id # Upload file to vector store - with open_with_lf(asset_file_path, "rb") as f: + with self.open_with_lf(asset_file_path, "rb") as f: file = await openai_client.vector_stores.files.upload_and_poll( vector_store_id=vector_store.id, file=f, diff --git a/sdk/ai/azure-ai-projects/tests/test_base.py b/sdk/ai/azure-ai-projects/tests/test_base.py index 50130827665f..27602d600426 100644 --- a/sdk/ai/azure-ai-projects/tests/test_base.py +++ b/sdk/ai/azure-ai-projects/tests/test_base.py @@ -39,64 +39,6 @@ _BUILTIN_OPEN = open -@overload -def open_with_lf( - file: Union[str, bytes, os.PathLike, int], - mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"] = "r", - buffering: int = -1, - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, - closefd: bool = True, - opener: Optional[Any] = None, -) -> TextIO: ... - - -@overload -def open_with_lf( - file: Union[str, bytes, os.PathLike, int], - mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"], - buffering: int = -1, - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, - closefd: bool = True, - opener: Optional[Any] = None, -) -> BinaryIO: ... - - -@overload -def open_with_lf( - file: Union[str, bytes, os.PathLike, int], - mode: str, - buffering: int = -1, - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, - closefd: bool = True, - opener: Optional[Any] = None, -) -> IO[Any]: ... - - -def open_with_lf( - file: Union[str, bytes, os.PathLike, int], - mode: str = "r", - buffering: int = -1, - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, - closefd: bool = True, - opener: Optional[Any] = None, -) -> IO[Any]: - """ - Open function that converts CRLF to LF for text files. - - This function has the same signature as built-in open and converts line endings - to ensure consistent behavior during test recording and playback. - """ - return patched_open_crlf_to_lf(file, mode, buffering, encoding, errors, newline, closefd, opener) - - # Load secrets from environment variables servicePreparer = functools.partial( EnvironmentVariableLoader, @@ -271,6 +213,64 @@ class TestBase(AzureRecordedTestCase): r"^InstrumentationKey=[0-9a-fA-F-]{36};IngestionEndpoint=https://.+.applicationinsights.azure.com/;LiveEndpoint=https://.+.monitor.azure.com/;ApplicationId=[0-9a-fA-F-]{36}$" ) + @overload + def open_with_lf( + self, + file: Union[str, bytes, os.PathLike, int], + mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"] = "r", + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + closefd: bool = True, + opener: Optional[Any] = None, + ) -> TextIO: ... + + @overload + def open_with_lf( + self, + file: Union[str, bytes, os.PathLike, int], + mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"], + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + closefd: bool = True, + opener: Optional[Any] = None, + ) -> BinaryIO: ... + + @overload + def open_with_lf( + self, + file: Union[str, bytes, os.PathLike, int], + mode: str, + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + closefd: bool = True, + opener: Optional[Any] = None, + ) -> IO[Any]: ... + + def open_with_lf( + self, + file: Union[str, bytes, os.PathLike, int], + mode: str = "r", + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + closefd: bool = True, + opener: Optional[Any] = None, + ) -> IO[Any]: + """ + Open function that converts CRLF to LF for text files. + + This function has the same signature as built-in open and converts line endings + to ensure consistent behavior during test recording and playback. + """ + return patched_open_crlf_to_lf(file, mode, buffering, encoding, errors, newline, closefd, opener) + # helper function: create projects client using environment variables def create_client(self, *, operation_group: Optional[str] = None, **kwargs) -> AIProjectClient: # fetch environment variables From 18cceb660b0f666d52be81910aeabc6945dae88d Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Tue, 9 Dec 2025 09:02:27 -0800 Subject: [PATCH 8/9] Resolved comments --- .../tests/samples/test_samples.py | 12 +- sdk/ai/azure-ai-projects/tests/test_base.py | 122 +++++++++--------- 2 files changed, 68 insertions(+), 66 deletions(-) diff --git a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py index a60773a0b691..a20c75c9eb25 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -9,7 +9,7 @@ from azure.core.exceptions import HttpResponseError from devtools_testutils.aio import recorded_by_proxy_async from devtools_testutils import AzureRecordedTestCase, recorded_by_proxy, RecordedTransport -from test_base import servicePreparer, patched_open_crlf_to_lf +from test_base import TestBase, servicePreparer from pytest import MonkeyPatch from azure.ai.projects import AIProjectClient @@ -17,9 +17,7 @@ class SampleExecutor: """Helper class for executing sample files with proper environment setup and credential mocking.""" - def __init__( - self, test_instance: "AzureRecordedTestCase", sample_path: str, env_var_mapping: dict[str, str], **kwargs - ): + def __init__(self, test_instance: "TestBase", sample_path: str, env_var_mapping: dict[str, str], **kwargs): self.test_instance = test_instance self.sample_path = sample_path self.print_calls: list[str] = [] @@ -58,7 +56,7 @@ def execute(self): with ( MonkeyPatch.context() as mp, mock.patch("builtins.print", side_effect=self._capture_print), - mock.patch("builtins.open", side_effect=patched_open_crlf_to_lf), + mock.patch("builtins.open", side_effect=self.test_instance.patched_open_crlf_to_lf), mock.patch("azure.identity.DefaultAzureCredential") as mock_credential, ): for var_name, var_value in self.env_vars.items(): @@ -79,7 +77,7 @@ async def execute_async(self): with ( MonkeyPatch.context() as mp, mock.patch("builtins.print", side_effect=self._capture_print), - mock.patch("builtins.open", side_effect=patched_open_crlf_to_lf), + mock.patch("builtins.open", side_effect=self.test_instance.patched_open_crlf_to_lf), mock.patch("azure.identity.aio.DefaultAzureCredential") as mock_credential, ): for var_name, var_value in self.env_vars.items(): @@ -205,7 +203,7 @@ def _get_tools_sample_paths_async(): return samples -class TestSamples(AzureRecordedTestCase): +class TestSamples(TestBase): _samples_folder_path: str _results: dict[str, tuple[bool, str]] diff --git a/sdk/ai/azure-ai-projects/tests/test_base.py b/sdk/ai/azure-ai-projects/tests/test_base.py index 27602d600426..eb7408224e32 100644 --- a/sdk/ai/azure-ai-projects/tests/test_base.py +++ b/sdk/ai/azure-ai-projects/tests/test_base.py @@ -68,65 +68,6 @@ DEVELOPER_TIER_TRAINING_TYPE: Final[str] = "developerTier" -def patched_open_crlf_to_lf(*args, **kwargs): - """ - Patched open function that converts CRLF to LF for text files. - - This function should be used with mock.patch("builtins.open", side_effect=TestBase.patched_open_crlf_to_lf) - to ensure consistent line endings in test files during recording and playback. - """ - # Extract file path - first positional arg or 'file' keyword arg - if args: - file_path = args[0] - elif "file" in kwargs: - file_path = kwargs["file"] - else: - # No file path provided, just pass through - return _BUILTIN_OPEN(*args, **kwargs) - - # Extract mode - second positional arg or 'mode' keyword arg - if len(args) > 1: - mode = args[1] - else: - mode = kwargs.get("mode", "r") - - # Check if this is binary read mode for text-like files - if "r" in mode and "b" in mode and file_path and isinstance(file_path, str): - # Check file extension to determine if it's a text file - text_extensions = {".txt", ".json", ".jsonl", ".csv", ".md", ".yaml", ".yml", ".xml"} - ext = os.path.splitext(file_path)[1].lower() - if ext in text_extensions: - # Read the original file - with _BUILTIN_OPEN(file_path, "rb") as f: - content = f.read() - - # Convert CRLF to LF - converted_content = content.replace(b"\r\n", b"\n") - - # Only create temp file if conversion was needed - if converted_content != content: - # Create a sub temp folder and save file with same filename - temp_dir = tempfile.mkdtemp() - original_filename = os.path.basename(file_path) - temp_path = os.path.join(temp_dir, original_filename) - - # Write the converted content to the temp file - with _BUILTIN_OPEN(temp_path, "wb") as temp_file: - temp_file.write(converted_content) - - # Replace file path with temp path - if args: - # File path was passed as positional arg - return _BUILTIN_OPEN(temp_path, *args[1:], **kwargs) - else: - # File path was passed as keyword arg - kwargs = kwargs.copy() - kwargs["file"] = temp_path - return _BUILTIN_OPEN(**kwargs) - - return _BUILTIN_OPEN(*args, **kwargs) - - class TestBase(AzureRecordedTestCase): test_redteams_params = { @@ -213,6 +154,69 @@ class TestBase(AzureRecordedTestCase): r"^InstrumentationKey=[0-9a-fA-F-]{36};IngestionEndpoint=https://.+.applicationinsights.azure.com/;LiveEndpoint=https://.+.monitor.azure.com/;ApplicationId=[0-9a-fA-F-]{36}$" ) + def patched_open_crlf_to_lf(self, *args, **kwargs): + """ + Patched open function that converts CRLF to LF for text files. + + This function should be used with mock.patch("builtins.open", side_effect=TestBase.patched_open_crlf_to_lf) + to ensure consistent line endings in test files during recording and playback. + + Note: CRLF to LF conversion is only performed when opening text-like files (.txt, .json, .jsonl, .csv, + .md, .yaml, .yml, .xml) in binary read mode ("rb"). For all other modes or file types, the call is + forwarded to the built-in open function as is. + """ + # Extract file path - first positional arg or 'file' keyword arg + if args: + file_path = args[0] + elif "file" in kwargs: + file_path = kwargs["file"] + else: + # No file path provided, just pass through + return _BUILTIN_OPEN(*args, **kwargs) + + # Extract mode - second positional arg or 'mode' keyword arg + if len(args) > 1: + mode = str(args[1]) + else: + mode = str(kwargs.get("mode", "r")) + + # Check if this is binary read mode for text-like files + if "r" in mode and "b" in mode and file_path and isinstance(file_path, str): + # Check file extension to determine if it's a text file + text_extensions = {".txt", ".json", ".jsonl", ".csv", ".md", ".yaml", ".yml", ".xml"} + ext = os.path.splitext(file_path)[1].lower() + if ext in text_extensions: + # Read the original file + with _BUILTIN_OPEN(file_path, "rb") as f: + content = f.read() + + # Convert CRLF to LF + converted_content = content.replace(b"\r\n", b"\n") + + # Only create temp file if conversion was needed + if converted_content != content: + # Create a sub temp folder and save file with same filename + temp_dir = tempfile.mkdtemp() + original_filename = os.path.basename(file_path) + temp_path = os.path.join(temp_dir, original_filename) + + # Write the converted content to the temp file + print(f"Converting CRLF to LF for {file_path} and saving to {temp_path}") + with _BUILTIN_OPEN(temp_path, "wb") as temp_file: + temp_file.write(converted_content) + + # Replace file path with temp path + if args: + # File path was passed as positional arg + return _BUILTIN_OPEN(temp_path, *args[1:], **kwargs) + else: + # File path was passed as keyword arg + kwargs = kwargs.copy() + kwargs["file"] = temp_path + return _BUILTIN_OPEN(**kwargs) + + return _BUILTIN_OPEN(*args, **kwargs) + @overload def open_with_lf( self, From 3d929840401a0febe78763520cb0449b1de36ae1 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Tue, 9 Dec 2025 09:25:39 -0800 Subject: [PATCH 9/9] Moved patched_open_crlf_to_lf outside of TestCase --- .../tests/samples/test_samples.py | 12 +- sdk/ai/azure-ai-projects/tests/test_base.py | 127 +++++++++--------- 2 files changed, 71 insertions(+), 68 deletions(-) diff --git a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py index a20c75c9eb25..a60773a0b691 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -9,7 +9,7 @@ from azure.core.exceptions import HttpResponseError from devtools_testutils.aio import recorded_by_proxy_async from devtools_testutils import AzureRecordedTestCase, recorded_by_proxy, RecordedTransport -from test_base import TestBase, servicePreparer +from test_base import servicePreparer, patched_open_crlf_to_lf from pytest import MonkeyPatch from azure.ai.projects import AIProjectClient @@ -17,7 +17,9 @@ class SampleExecutor: """Helper class for executing sample files with proper environment setup and credential mocking.""" - def __init__(self, test_instance: "TestBase", sample_path: str, env_var_mapping: dict[str, str], **kwargs): + def __init__( + self, test_instance: "AzureRecordedTestCase", sample_path: str, env_var_mapping: dict[str, str], **kwargs + ): self.test_instance = test_instance self.sample_path = sample_path self.print_calls: list[str] = [] @@ -56,7 +58,7 @@ def execute(self): with ( MonkeyPatch.context() as mp, mock.patch("builtins.print", side_effect=self._capture_print), - mock.patch("builtins.open", side_effect=self.test_instance.patched_open_crlf_to_lf), + mock.patch("builtins.open", side_effect=patched_open_crlf_to_lf), mock.patch("azure.identity.DefaultAzureCredential") as mock_credential, ): for var_name, var_value in self.env_vars.items(): @@ -77,7 +79,7 @@ async def execute_async(self): with ( MonkeyPatch.context() as mp, mock.patch("builtins.print", side_effect=self._capture_print), - mock.patch("builtins.open", side_effect=self.test_instance.patched_open_crlf_to_lf), + mock.patch("builtins.open", side_effect=patched_open_crlf_to_lf), mock.patch("azure.identity.aio.DefaultAzureCredential") as mock_credential, ): for var_name, var_value in self.env_vars.items(): @@ -203,7 +205,7 @@ def _get_tools_sample_paths_async(): return samples -class TestSamples(TestBase): +class TestSamples(AzureRecordedTestCase): _samples_folder_path: str _results: dict[str, tuple[bool, str]] diff --git a/sdk/ai/azure-ai-projects/tests/test_base.py b/sdk/ai/azure-ai-projects/tests/test_base.py index eb7408224e32..01bd34c7fcd6 100644 --- a/sdk/ai/azure-ai-projects/tests/test_base.py +++ b/sdk/ai/azure-ai-projects/tests/test_base.py @@ -68,6 +68,70 @@ DEVELOPER_TIER_TRAINING_TYPE: Final[str] = "developerTier" +def patched_open_crlf_to_lf(*args, **kwargs): + """ + Patched open function that converts CRLF to LF for text files. + + This function should be used with mock.patch("builtins.open", side_effect=TestBase.patched_open_crlf_to_lf) + to ensure consistent line endings in test files during recording and playback. + + Note: CRLF to LF conversion is only performed when opening text-like files (.txt, .json, .jsonl, .csv, + .md, .yaml, .yml, .xml) in binary read mode ("rb"). For all other modes or file types, the call is + forwarded to the built-in open function as is. + """ + # Extract file path - first positional arg or 'file' keyword arg + if args: + file_path = args[0] + elif "file" in kwargs: + file_path = kwargs["file"] + else: + # No file path provided, just pass through + return _BUILTIN_OPEN(*args, **kwargs) + + # Extract mode - second positional arg or 'mode' keyword arg + if len(args) > 1: + mode = str(args[1]) + else: + mode = str(kwargs.get("mode", "r")) + + # Check if this is binary read mode for text-like files + if "r" in mode and "b" in mode and file_path and isinstance(file_path, str): + # Check file extension to determine if it's a text file + text_extensions = {".txt", ".json", ".jsonl", ".csv", ".md", ".yaml", ".yml", ".xml"} + ext = os.path.splitext(file_path)[1].lower() + if ext in text_extensions: + # Read the original file + with _BUILTIN_OPEN(file_path, "rb") as f: + content = f.read() + + # Convert CRLF to LF + converted_content = content.replace(b"\r\n", b"\n") + + # Only create temp file if conversion was needed + if converted_content != content: + # Create a sub temp folder and save file with same filename + temp_dir = tempfile.mkdtemp() + original_filename = os.path.basename(file_path) + temp_path = os.path.join(temp_dir, original_filename) + + # Write the converted content to the temp file + print(f"Converting CRLF to LF for {file_path} and saving to {temp_path}") + with _BUILTIN_OPEN(temp_path, "wb") as temp_file: + temp_file.write(converted_content) + + # Replace file path with temp path + if args: + # File path was passed as positional arg + return _BUILTIN_OPEN(temp_path, *args[1:], **kwargs) + else: + # File path was passed as keyword arg + kwargs = kwargs.copy() + kwargs["file"] = temp_path + return _BUILTIN_OPEN(**kwargs) + + return _BUILTIN_OPEN(*args, **kwargs) + + class TestBase(AzureRecordedTestCase): test_redteams_params = { @@ -154,69 +218,6 @@ class TestBase(AzureRecordedTestCase): r"^InstrumentationKey=[0-9a-fA-F-]{36};IngestionEndpoint=https://.+.applicationinsights.azure.com/;LiveEndpoint=https://.+.monitor.azure.com/;ApplicationId=[0-9a-fA-F-]{36}$" ) - def patched_open_crlf_to_lf(self, *args, **kwargs): - """ - Patched open function that converts CRLF to LF for text files. - - This function should be used with mock.patch("builtins.open", side_effect=TestBase.patched_open_crlf_to_lf) - to ensure consistent line endings in test files during recording and playback. - - Note: CRLF to LF conversion is only performed when opening text-like files (.txt, .json, .jsonl, .csv, - .md, .yaml, .yml, .xml) in binary read mode ("rb"). For all other modes or file types, the call is - forwarded to the built-in open function as is. - """ - # Extract file path - first positional arg or 'file' keyword arg - if args: - file_path = args[0] - elif "file" in kwargs: - file_path = kwargs["file"] - else: - # No file path provided, just pass through - return _BUILTIN_OPEN(*args, **kwargs) - - # Extract mode - second positional arg or 'mode' keyword arg - if len(args) > 1: - mode = str(args[1]) - else: - mode = str(kwargs.get("mode", "r")) - - # Check if this is binary read mode for text-like files - if "r" in mode and "b" in mode and file_path and isinstance(file_path, str): - # Check file extension to determine if it's a text file - text_extensions = {".txt", ".json", ".jsonl", ".csv", ".md", ".yaml", ".yml", ".xml"} - ext = os.path.splitext(file_path)[1].lower() - if ext in text_extensions: - # Read the original file - with _BUILTIN_OPEN(file_path, "rb") as f: - content = f.read() - - # Convert CRLF to LF - converted_content = content.replace(b"\r\n", b"\n") - - # Only create temp file if conversion was needed - if converted_content != content: - # Create a sub temp folder and save file with same filename - temp_dir = tempfile.mkdtemp() - original_filename = os.path.basename(file_path) - temp_path = os.path.join(temp_dir, original_filename) - - # Write the converted content to the temp file - print(f"Converting CRLF to LF for {file_path} and saving to {temp_path}") - with _BUILTIN_OPEN(temp_path, "wb") as temp_file: - temp_file.write(converted_content) - - # Replace file path with temp path - if args: - # File path was passed as positional arg - return _BUILTIN_OPEN(temp_path, *args[1:], **kwargs) - else: - # File path was passed as keyword arg - kwargs = kwargs.copy() - kwargs["file"] = temp_path - return _BUILTIN_OPEN(**kwargs) - - return _BUILTIN_OPEN(*args, **kwargs) - @overload def open_with_lf( self,