Skip to content

Commit b4d2b78

Browse files
committed
feat: POC automation test
JIRA: QA-23477 risk: nonprod
1 parent a576d4f commit b4d2b78

12 files changed

+399
-1
lines changed

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.12.4
1+
3.11.4
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"id": "sum_of_amount_by_stage_name",
3+
"title": "Sum of Amount by Stage Name",
4+
"visualizationType": "COLUMN",
5+
"metrics": [
6+
{
7+
"id": "f_opportunitysnapshot.f_amount",
8+
"type": "fact",
9+
"title": "Sum of Amount",
10+
"aggFunction": "SUM"
11+
}
12+
],
13+
"dimensionality": [
14+
{
15+
"id": "attr.f_stage.stagename",
16+
"type": "attribute",
17+
"title": "Stage Name"
18+
}
19+
],
20+
"filters": []
21+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"id": "best_case_by_product_for_direct_sales_department",
3+
"title": "Best Case by Product for Direct Sales Department",
4+
"visualizationType": "COLUMN",
5+
"metrics": [
6+
{
7+
"id": "best_case",
8+
"type": "metric",
9+
"title": "Best Case"
10+
}
11+
],
12+
"dimensionality": [
13+
{
14+
"id": "attr.f_product.product",
15+
"type": "attribute",
16+
"title": "Product"
17+
}
18+
],
19+
"filters": [
20+
{
21+
"using": "f_owner.department_id",
22+
"include": [
23+
"Direct Sales"
24+
]
25+
}
26+
]
27+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"id": "total_amount",
3+
"title": "Total Amount",
4+
"visualizationType": "HEADLINE",
5+
"metrics": [
6+
{
7+
"id": "f_opportunitysnapshot.f_amount",
8+
"type": "fact",
9+
"title": "Sum of Amount",
10+
"aggFunction": "SUM"
11+
}
12+
],
13+
"dimensionality": [],
14+
"filters": []
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# (C) 2024 GoodData Corporation
2+
# env.py
3+
import os
4+
5+
# Define environment variables
6+
HOST = os.getenv("GOODDATA_HOST", "")
7+
TOKEN = os.getenv("GOODDATA_TOKEN", "")
8+
DATASOURCE_ID = os.getenv("DATASOURCE_ID", "")
9+
WORKSPACE_ID = ""
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"question": "generate HEADLINE showing Sum of Amount",
4+
"expected_objects_file": "headline_sum_amount.json"
5+
},
6+
{
7+
"question": "generate COLUMN chart showing Sum of Amount sliced by Stage Name",
8+
"expected_objects_file": "column_sum_amount_by_stage_name.json"
9+
},
10+
{
11+
"question": "generate COLUMN chart showing Best Case sliced by Product filtered by Department is 'Direct Sales'",
12+
"expected_objects_file": "column_sum_best_case_by_product_filter_department_direct_sales.json"
13+
}
14+
]
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# (C) 2024 GoodData Corporation
2+
import json
3+
import os
4+
import sys
5+
from pprint import pprint
6+
7+
import gooddata_api_client
8+
import pytest
9+
from gooddata_api_client.api import smart_functions_api
10+
from gooddata_api_client.model.chat_history_request import ChatHistoryRequest
11+
from gooddata_api_client.model.chat_history_result import ChatHistoryResult
12+
from gooddata_api_client.model.chat_request import ChatRequest
13+
14+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15+
16+
from env import HOST, TOKEN, WORKSPACE_ID
17+
18+
EXPECTED_OBJECTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data_response")
19+
20+
QUESTIONS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "fixtures")
21+
22+
23+
@pytest.fixture(scope="module")
24+
def api_client():
25+
configuration = gooddata_api_client.Configuration(host=HOST)
26+
configuration.access_token = TOKEN
27+
with gooddata_api_client.ApiClient(configuration) as api_client:
28+
yield api_client
29+
30+
31+
class GoodDataAiChatApp:
32+
def __init__(self, api_client, workspace_id):
33+
self.api_instance = smart_functions_api.SmartFunctionsApi(api_client)
34+
self.workspace_id = workspace_id
35+
36+
async def ask_question(self, question: str):
37+
chat_request = ChatRequest(question=question)
38+
return self.api_instance.ai_chat(self.workspace_id, chat_request)
39+
40+
async def chat_history(self, chat_history_interaction_id: int, user_feedback: str):
41+
chat_history_request = ChatHistoryRequest(
42+
chat_history_interaction_id=chat_history_interaction_id,
43+
user_feedback=user_feedback,
44+
)
45+
return self.api_instance.ai_chat_history(self.workspace_id, chat_history_request)
46+
47+
48+
def set_authorization_header(api_client, token):
49+
api_client.default_headers["Authorization"] = f"Bearer {token}"
50+
51+
52+
def snake_to_camel(snake_str):
53+
"""Convert snake_case to camelCase."""
54+
components = snake_str.split("_")
55+
return components[0] + "".join(x.title() for x in components[1:])
56+
57+
58+
def normalize_metrics(metrics, exclude_keys=None):
59+
"""Normalize keys in the metrics list, excluding specified keys."""
60+
if exclude_keys is None:
61+
exclude_keys = []
62+
normalized_metrics = []
63+
for metric in metrics:
64+
if isinstance(metric, dict):
65+
normalized_metric = {}
66+
for key, value in metric.items():
67+
if key in exclude_keys:
68+
continue
69+
else:
70+
new_key = snake_to_camel(key)
71+
normalized_metric[new_key] = value
72+
normalized_metrics.append(normalized_metric)
73+
return normalized_metrics
74+
75+
76+
def handle_api_response_ai_chat(api_response, expected_objects):
77+
response_dict = api_response.to_dict()
78+
79+
# Normalize the keys in the actual and expected metrics, excluding 'title'
80+
actual_metrics = normalize_metrics(
81+
response_dict["created_visualizations"]["objects"][0]["metrics"], exclude_keys=["title"]
82+
)
83+
expected_metrics = normalize_metrics(expected_objects["metrics"], exclude_keys=["title"])
84+
85+
assert actual_metrics == expected_metrics, "Metrics do not match"
86+
assert (
87+
response_dict["created_visualizations"]["objects"][0]["visualization_type"]
88+
== expected_objects["visualizationType"]
89+
), "Visualization type does not match"
90+
assert (
91+
response_dict["created_visualizations"]["objects"][0]["dimensionality"] == expected_objects["dimensionality"]
92+
), "Dimensionality does not match"
93+
assert (
94+
response_dict["created_visualizations"]["objects"][0]["filters"] == expected_objects["filters"]
95+
), "Filters do not match"
96+
97+
98+
def getChatHistoryInteractionId(api_response):
99+
chat_history_interaction_id = api_response.chat_history_interaction_id
100+
print(f"Chat history interaction id: {chat_history_interaction_id}")
101+
return chat_history_interaction_id
102+
103+
104+
@pytest.fixture(scope="module")
105+
def app(api_client):
106+
app = GoodDataAiChatApp(api_client, WORKSPACE_ID)
107+
set_authorization_header(api_client, TOKEN)
108+
return app
109+
110+
111+
def load_expected_objects(fixture_folder, filename):
112+
with open(os.path.join(fixture_folder, filename)) as file:
113+
return json.load(file)
114+
115+
116+
questions_list = load_expected_objects(QUESTIONS_DIR, "ai_questions.json")
117+
118+
119+
@pytest.mark.parametrize(
120+
"question,expected_objects_file",
121+
[(item["question"], item["expected_objects_file"]) for item in questions_list],
122+
ids=[item["question"] for item in questions_list],
123+
)
124+
@pytest.mark.asyncio
125+
async def test_ai_chat(app, question, expected_objects_file):
126+
expected_objects = load_expected_objects(EXPECTED_OBJECTS_DIR, expected_objects_file)
127+
try:
128+
api_response = await app.ask_question(question)
129+
handle_api_response_ai_chat(api_response, expected_objects)
130+
except gooddata_api_client.ApiException as e:
131+
pytest.fail(f"Exception when calling SmartFunctionsApi->ai_chat: {e}\n")
132+
except Exception as e:
133+
pytest.fail(f"An unexpected error occurred: {e}\n")
134+
135+
136+
@pytest.mark.asyncio
137+
async def test_ai_chat_history(app):
138+
try:
139+
api_response = await app.chat_history(1260, "POSITIVE")
140+
141+
assert isinstance(api_response, ChatHistoryResult), "Response is not of type ChatHistoryResult"
142+
pprint(api_response.to_dict())
143+
except gooddata_api_client.ApiException as e:
144+
print(f"Exception when calling SmartFunctionsApi->ai_chat_history: {e}")
145+
pytest.fail(f"Exception when calling SmartFunctionsApi->ai_chat_history: {e}\n")
146+
except Exception as e:
147+
print(f"An unexpected error occurred: {e}")
148+
pytest.fail(f"An unexpected error occurred: {e}\n")
149+
150+
151+
if __name__ == "__main__":
152+
pytest.main()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# (C) 2024 GoodData Corporation
2+
import os
3+
import sys
4+
5+
import pytest
6+
7+
# Add the root directory to sys.path
8+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9+
10+
from env import HOST, TOKEN, WORKSPACE_ID
11+
from gooddata_sdk import GoodDataSdk
12+
13+
14+
@pytest.fixture
15+
def test_config():
16+
return {"host": HOST, "token": TOKEN, "workspace_id": WORKSPACE_ID}
17+
18+
19+
questions = [
20+
"What is the number of Accounts?",
21+
"What is the total of Amount?",
22+
]
23+
24+
25+
@pytest.mark.parametrize("question", questions)
26+
def test_ask_ai(test_config, question):
27+
sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"])
28+
workspace_id = test_config["workspace_id"]
29+
chat_ai_res = sdk.compute.ai_chat(workspace_id, question=question)
30+
31+
print(f"Chat AI response: {chat_ai_res}")
32+
assert chat_ai_res is not None, "Response should not be None"
33+
34+
35+
if __name__ == "__main__":
36+
pytest.main()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# (C) 2024 GoodData Corporation
2+
from env import DATASOURCE_ID, HOST, TOKEN, WORKSPACE_ID
3+
from workspace_manager import createWorkspace, getDataSource, update_env_file
4+
5+
if __name__ == "__main__":
6+
test_config = {"host": HOST, "token": TOKEN}
7+
8+
if WORKSPACE_ID:
9+
print(f"Workspace ID '{WORKSPACE_ID}' already exists. Skipping workspace creation.")
10+
else:
11+
workspace_id = createWorkspace(test_config)
12+
dataSource = getDataSource(DATASOURCE_ID, test_config)
13+
if workspace_id:
14+
update_env_file(workspace_id)
15+
else:
16+
print("Failed to create workspace.")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# (C) 2024 GoodData Corporation
2+
from env import HOST, TOKEN
3+
from workspace_manager import deleteWorkspace
4+
5+
if __name__ == "__main__":
6+
test_config = {"host": HOST, "token": TOKEN}
7+
8+
deleteWorkspace(test_config)

0 commit comments

Comments
 (0)