Skip to content

Commit 716a708

Browse files
lumoslntLina Tang
and
Lina Tang
authored
Refine multimedia e2e tests (#863)
# Description Refine multimedia e2e tests. # All Promptflow Contribution checklist: - [x] **The pull request does not introduce [breaking changes].** - [ ] **CHANGELOG is updated for new features, bug fixes or other significant changes.** - [x] **I have read the [contribution guidelines](../CONTRIBUTING.md).** - [ ] **Create an issue and link to the pull request to get dedicated review from promptflow team. Learn more: [suggested workflow](../CONTRIBUTING.md#suggested-workflow).** ## General Guidelines and Best Practices - [x] Title of the pull request is clear and informative. - [x] There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md). ### Testing Guidelines - [x] Pull request includes test coverage for the included changes. --------- Co-authored-by: Lina Tang <[email protected]>
1 parent b638eee commit 716a708

File tree

22 files changed

+241
-198
lines changed

22 files changed

+241
-198
lines changed

src/promptflow/tests/executor/e2etests/test_executor_happypath.py

-76
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import uuid
2-
import os
32
from types import GeneratorType
4-
from pathlib import Path
53

64
import pytest
75

@@ -13,7 +11,6 @@
1311
from promptflow.executor import FlowExecutor
1412
from promptflow.executor._errors import ConnectionNotFound, InputTypeError, ResolveToolError
1513
from promptflow.executor.flow_executor import BulkResult, LineResult
16-
from promptflow.storage._run_storage import DefaultRunStorage
1714
from promptflow.storage import AbstractRunStorage
1815

1916
from ..utils import (
@@ -22,7 +19,6 @@
2219
get_flow_expected_status_summary,
2320
get_flow_sample_inputs,
2421
get_yaml_file,
25-
get_yaml_working_dir
2622
)
2723

2824
SAMPLE_FLOW = "web_classification_no_variants"
@@ -31,11 +27,6 @@
3127
SAMPLE_FLOW_WITH_LANGCHAIN_TRACES = "flow_with_langchain_traces"
3228

3329

34-
def assert_contains_substrings(s, substrings):
35-
for substring in substrings:
36-
assert substring in s
37-
38-
3930
class MemoryRunStorage(AbstractRunStorage):
4031
def __init__(self):
4132
self._node_runs = {}
@@ -231,38 +222,6 @@ def test_executor_exec_line(self, flow_folder, dev_connections):
231222
assert node_run_info.node == node
232223
assert isinstance(node_run_info.api_calls, list) # api calls is set
233224

234-
@pytest.mark.parametrize(
235-
"flow_folder",
236-
[
237-
"python_tool_with_multiple_image_nodes"
238-
],
239-
)
240-
def test_executor_exec_line_with_image(self, flow_folder, dev_connections):
241-
self.skip_serp(flow_folder, dev_connections)
242-
working_dir = get_yaml_working_dir(flow_folder)
243-
os.chdir(working_dir)
244-
storage = DefaultRunStorage(base_dir=working_dir, sub_dir=Path("./temp"))
245-
executor = FlowExecutor.create(get_yaml_file(flow_folder), dev_connections, storage=storage)
246-
flow_result = executor.exec_line({})
247-
assert not executor._run_tracker._flow_runs, "Flow runs in run tracker should be empty."
248-
assert not executor._run_tracker._node_runs, "Node runs in run tracker should be empty."
249-
assert isinstance(flow_result.output, dict)
250-
assert flow_result.run_info.status == Status.Completed
251-
node_count = len(executor._flow.nodes)
252-
assert isinstance(flow_result.run_info.api_calls, list) and len(flow_result.run_info.api_calls) == node_count
253-
substrings = ["data:image/jpg;path", ".jpg"]
254-
for i in range(node_count):
255-
assert_contains_substrings(str(flow_result.run_info.api_calls[i]), substrings)
256-
assert len(flow_result.node_run_infos) == node_count
257-
for node, node_run_info in flow_result.node_run_infos.items():
258-
assert node_run_info.status == Status.Completed
259-
assert node_run_info.node == node
260-
assert isinstance(node_run_info.api_calls, list) # api calls is set
261-
assert_contains_substrings(str(node_run_info.inputs), substrings)
262-
assert_contains_substrings(str(node_run_info.output), substrings)
263-
assert_contains_substrings(str(node_run_info.result), substrings)
264-
assert_contains_substrings(str(node_run_info.api_calls[0]), substrings)
265-
266225
@pytest.mark.parametrize(
267226
"flow_folder, node_name, flow_inputs, dependency_nodes_outputs",
268227
[
@@ -294,41 +253,6 @@ def test_executor_exec_node(self, flow_folder, node_name, flow_inputs, dependenc
294253
assert run_info.node == node_name
295254
assert run_info.system_metrics["duration"] >= 0
296255

297-
@pytest.mark.parametrize(
298-
"flow_folder, node_name, flow_inputs, dependency_nodes_outputs",
299-
[
300-
("python_tool_with_multiple_image_nodes", "python_node_2", {"logo_content": "Microsoft and four squares"},
301-
{"python_node": {"image": {"data:image/jpg;path": "logo.jpg"}, "image_name": "Microsoft's logo",
302-
"image_list": [{"data:image/jpg;path": "logo.jpg"}]}}),
303-
("python_tool_with_multiple_image_nodes", "python_node", {
304-
"image": "logo.jpg", "image_name": "Microsoft's logo"}, {},)
305-
],
306-
)
307-
def test_executor_exec_node_with_image(self, flow_folder, node_name, flow_inputs, dependency_nodes_outputs,
308-
dev_connections):
309-
self.skip_serp(flow_folder, dev_connections)
310-
yaml_file = get_yaml_file(flow_folder)
311-
working_dir = get_yaml_working_dir(flow_folder)
312-
os.chdir(working_dir)
313-
run_info = FlowExecutor.load_and_exec_node(
314-
yaml_file,
315-
node_name,
316-
flow_inputs=flow_inputs,
317-
dependency_nodes_outputs=dependency_nodes_outputs,
318-
connections=dev_connections,
319-
output_sub_dir=("./temp"),
320-
raise_ex=True,
321-
)
322-
substrings = ["data:image/jpg;path", "temp", ".jpg"]
323-
assert_contains_substrings(str(run_info.inputs), substrings)
324-
assert_contains_substrings(str(run_info.output), substrings)
325-
assert_contains_substrings(str(run_info.result), substrings)
326-
assert_contains_substrings(str(run_info.api_calls[0]), substrings)
327-
assert run_info.status == Status.Completed
328-
assert isinstance(run_info.api_calls, list)
329-
assert run_info.node == node_name
330-
assert run_info.system_metrics["duration"] >= 0
331-
332256
def test_executor_node_overrides(self, dev_connections):
333257
inputs = self.get_line_inputs()
334258
executor = FlowExecutor.create(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import os
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from promptflow._utils.multimedia_utils import _create_image_from_file, _is_multimedia_dict
7+
from promptflow.contracts.multimedia import Image
8+
from promptflow.contracts.run_info import Status
9+
from promptflow.executor import FlowExecutor
10+
from promptflow.storage._run_storage import DefaultRunStorage
11+
12+
from ..utils import FLOW_ROOT, get_yaml_file, get_yaml_working_dir
13+
14+
SIMPLE_IMAGE_FLOW = "python_tool_with_simple_image"
15+
SIMPLE_IMAGE_FLOW_PATH = FLOW_ROOT / "python_tool_with_simple_image"
16+
COMPOSITE_IMAGE_FLOW = "python_tool_with_composite_image"
17+
COMPOSITE_IMAGE_FLOW_PATH = FLOW_ROOT / "python_tool_with_composite_image"
18+
IMAGE_URL = (
19+
"https://github.com/microsoft/promptflow/blob/93776a0631abf991896ab07d294f62082d5df3f3/src"
20+
"/promptflow/tests/test_configs/datas/test_image.jpg?raw=true"
21+
)
22+
23+
24+
def get_test_cases_for_simple_input():
25+
image = _create_image_from_file(SIMPLE_IMAGE_FLOW_PATH / "logo.jpg")
26+
inputs = [
27+
{"data:image/jpg;path": str(SIMPLE_IMAGE_FLOW_PATH / "logo.jpg")},
28+
{"data:image/jpg;base64": image.to_base64()},
29+
{"data:image/jpg;url": IMAGE_URL},
30+
str(SIMPLE_IMAGE_FLOW_PATH / "logo.jpg"),
31+
image.to_base64(),
32+
IMAGE_URL,
33+
]
34+
return [(SIMPLE_IMAGE_FLOW, {"image": input}) for input in inputs]
35+
36+
37+
def get_test_cases_for_composite_input():
38+
image_1 = _create_image_from_file(COMPOSITE_IMAGE_FLOW_PATH / "logo.jpg")
39+
image_2 = _create_image_from_file(COMPOSITE_IMAGE_FLOW_PATH / "logo_2.png")
40+
inputs = [
41+
[
42+
{"data:image/jpg;path": str(COMPOSITE_IMAGE_FLOW_PATH / "logo.jpg")},
43+
{"data:image/png;path": str(COMPOSITE_IMAGE_FLOW_PATH / "logo_2.png")}
44+
],
45+
[{"data:image/jpg;base64": image_1.to_base64()}, {"data:image/png;base64": image_2.to_base64()}],
46+
[{"data:image/jpg;url": IMAGE_URL}, {"data:image/png;url": IMAGE_URL}],
47+
]
48+
return [
49+
(COMPOSITE_IMAGE_FLOW, {"image_list": input, "image_dict": {"image_1": input[0], "image_2": input[1]}})
50+
for input in inputs
51+
]
52+
53+
54+
def get_test_cases_for_node_run():
55+
image = {"data:image/jpg;path": str(SIMPLE_IMAGE_FLOW_PATH / "logo.jpg")}
56+
simple_image_input = {"image": image}
57+
image_list = [{"data:image/jpg;path": "logo.jpg"}, {"data:image/png;path": "logo_2.png"}]
58+
image_dict = {
59+
"image_dict": {
60+
"image_1": {"data:image/jpg;path": "logo.jpg"},
61+
"image_2": {"data:image/png;path": "logo_2.png"},
62+
}
63+
}
64+
composite_image_input = {"image_list": image_list, "image_dcit": image_dict}
65+
66+
return [
67+
(SIMPLE_IMAGE_FLOW, "python_node", simple_image_input, None),
68+
(SIMPLE_IMAGE_FLOW, "python_node_2", simple_image_input, {"python_node": image}),
69+
(COMPOSITE_IMAGE_FLOW, "python_node", composite_image_input, None),
70+
(COMPOSITE_IMAGE_FLOW, "python_node_2", composite_image_input, None),
71+
(
72+
COMPOSITE_IMAGE_FLOW, "python_node_3", composite_image_input,
73+
{"python_node": image_list, "python_node_2": image_dict}
74+
),
75+
]
76+
77+
78+
def assert_contain_image_reference(value):
79+
assert not isinstance(value, Image)
80+
if isinstance(value, list):
81+
for item in value:
82+
assert_contain_image_reference(item)
83+
elif isinstance(value, dict):
84+
if _is_multimedia_dict(value):
85+
path = list(value.values())[0]
86+
assert isinstance(path, str)
87+
assert path.endswith(".jpg") or path.endswith(".jpeg") or path.endswith(".png")
88+
else:
89+
for _, v in value.items():
90+
assert_contain_image_reference(v)
91+
92+
93+
def assert_contain_image_object(value):
94+
if isinstance(value, list):
95+
for item in value:
96+
assert_contain_image_object(item)
97+
elif isinstance(value, dict):
98+
assert not _is_multimedia_dict(value)
99+
for _, v in value.items():
100+
assert_contain_image_object(v)
101+
else:
102+
assert isinstance(value, Image)
103+
104+
105+
@pytest.mark.usefixtures("dev_connections")
106+
@pytest.mark.e2etest
107+
class TestExecutorWithImage:
108+
@pytest.mark.parametrize(
109+
"flow_folder, inputs", get_test_cases_for_simple_input() + get_test_cases_for_composite_input()
110+
)
111+
def test_executor_exec_line_with_image(self, flow_folder, inputs, dev_connections):
112+
working_dir = get_yaml_working_dir(flow_folder)
113+
os.chdir(working_dir)
114+
storage = DefaultRunStorage(base_dir=working_dir, sub_dir=Path("./temp"))
115+
executor = FlowExecutor.create(get_yaml_file(flow_folder), dev_connections, storage=storage)
116+
flow_result = executor.exec_line(inputs)
117+
assert isinstance(flow_result.output, dict)
118+
assert_contain_image_object(flow_result.output)
119+
assert flow_result.run_info.status == Status.Completed
120+
assert_contain_image_reference(flow_result.run_info)
121+
for _, node_run_info in flow_result.node_run_infos.items():
122+
assert node_run_info.status == Status.Completed
123+
assert_contain_image_reference(node_run_info)
124+
125+
@pytest.mark.parametrize(
126+
"flow_folder, node_name, flow_inputs, dependency_nodes_outputs", get_test_cases_for_node_run()
127+
)
128+
def test_executor_exec_node_with_image(self, flow_folder, node_name, flow_inputs, dependency_nodes_outputs,
129+
dev_connections):
130+
working_dir = get_yaml_working_dir(flow_folder)
131+
os.chdir(working_dir)
132+
run_info = FlowExecutor.load_and_exec_node(
133+
get_yaml_file(flow_folder),
134+
node_name,
135+
flow_inputs=flow_inputs,
136+
dependency_nodes_outputs=dependency_nodes_outputs,
137+
connections=dev_connections,
138+
output_sub_dir=("./temp"),
139+
raise_ex=True,
140+
)
141+
assert run_info.status == Status.Completed
142+
assert_contain_image_reference(run_info)

src/promptflow/tests/sdk_cli_test/e2etests/test_cli.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1401,9 +1401,9 @@ def test_flow_test_with_image_input_and_output(self):
14011401
"flow",
14021402
"test",
14031403
"--flow",
1404-
f"{FLOWS_DIR}/python_tool_with_image_input_and_output",
1404+
f"{FLOWS_DIR}/python_tool_with_simple_image",
14051405
)
1406-
output_path = Path(FLOWS_DIR) / "python_tool_with_image_input_and_output" / ".promptflow" / "output"
1406+
output_path = Path(FLOWS_DIR) / "python_tool_with_simple_image" / ".promptflow" / "output"
14071407
assert output_path.exists()
1408-
image_path = Path(FLOWS_DIR) / "python_tool_with_image_input_and_output" / ".promptflow" / "intermediate"
1408+
image_path = Path(FLOWS_DIR) / "python_tool_with_simple_image" / ".promptflow" / "intermediate"
14091409
assert image_path.exists()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
inputs:
2+
image_list:
3+
type: list
4+
default:
5+
- data:image/jpg;path: logo.jpg
6+
- data:image/png;path: logo_2.png
7+
image_dict:
8+
type: object
9+
default:
10+
- image_1:
11+
data:image/jpg;path: logo.jpg
12+
- image_2:
13+
data:image/png;path: logo_2.png
14+
outputs:
15+
output:
16+
type: list
17+
reference: ${python_node_3.output}
18+
nodes:
19+
- name: python_node
20+
type: python
21+
source:
22+
type: code
23+
path: passthrough_list.py
24+
inputs:
25+
image_list: ${inputs.image_list}
26+
image_dict: ${inputs.image_dict}
27+
- name: python_node_2
28+
type: python
29+
source:
30+
type: code
31+
path: passthrough_dict.py
32+
inputs:
33+
image_list:
34+
- data:image/jpg;path: logo.jpg
35+
- data:image/png;path: logo_2.png
36+
image_dict:
37+
- image_1:
38+
data:image/jpg;path: logo.jpg
39+
- image_2:
40+
data:image/png;path: logo_2.png
41+
- name: python_node_3
42+
type: python
43+
source:
44+
type: code
45+
path: passthrough_list.py
46+
inputs:
47+
image_list: ${python_node.output}
48+
image_dict: ${python_node_2.output}
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from promptflow import tool
2+
3+
4+
@tool
5+
def passthrough_dict(image_list: list,image_dict: dict) -> list:
6+
return image_dict
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from promptflow import tool
2+
3+
@tool
4+
def passthrough_list(image_list: list, image_dict: dict):
5+
return image_list

src/promptflow/tests/test_configs/flows/python_tool_with_image_input_and_output/flow.dag.yaml

-16
This file was deleted.

src/promptflow/tests/test_configs/flows/python_tool_with_image_input_and_output/python_with_image.py

-7
This file was deleted.

src/promptflow/tests/test_configs/flows/python_tool_with_image_list/flow.dag.yaml

-33
This file was deleted.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)