blob: 4ea95890ee481072628cc3fc3e66b503993fd489 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
#include "service/query-profile-ai-analysis.h"
#include <atomic>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include <gflags/gflags_declare.h>
#include <rapidjson/document.h>
#include "testutil/death-test-util.h"
#include "testutil/gtest-util.h"
#include "testutil/scoped-flag-setter.h"
using std::string;
using std::string_view;
using std::vector;
DECLARE_int32(query_profile_ai_analysis_max_concurrent_runs);
namespace impala {
namespace {
using test::FindAssistantMessageInResponse;
using test::ParseJsonObjectFromResponse;
using test::ReleaseAiAnalysisSlot;
using test::TryAcquireAiAnalysisSlot;
using test::TryExtractAssistantContentText;
TEST(QueryProfileAiAnalysisHelpersTest, ParseJsonObjectFromNoisyResponse) {
constexpr string_view noisy_response =
"model debug prefix\n"
"{\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"final\"}}]}\n"
"model suffix";
rapidjson::Document response_doc;
EXPECT_TRUE(ParseJsonObjectFromResponse(noisy_response, &response_doc));
ASSERT_TRUE(response_doc.IsObject());
ASSERT_TRUE(response_doc.HasMember("choices"));
ASSERT_TRUE(response_doc["choices"].IsArray());
}
TEST(QueryProfileAiAnalysisHelpersTest, ParseJsonObjectFromResponseRejectsNonJsonText) {
constexpr string_view non_json_response = "there is no json object in this response";
rapidjson::Document response_doc;
EXPECT_FALSE(ParseJsonObjectFromResponse(non_json_response, &response_doc));
}
TEST(QueryProfileAiAnalysisHelpersTest, ExtractAssistantContentFromNestedArrayParts) {
constexpr string_view response = R"json(
prefix
{"choices":[{"message":{"content":[
{"text":"first"},
{"output_text":"second"},
{"text":{"value":"third"}},
{"text":null},
7,
{"foo":"bar"}
]}}]}
suffix
)json";
rapidjson::Document response_doc;
ASSERT_TRUE(ParseJsonObjectFromResponse(response, &response_doc));
const rapidjson::Value* assistant_message =
FindAssistantMessageInResponse(response_doc);
ASSERT_NE(nullptr, assistant_message);
string assistant_content = "stale";
EXPECT_TRUE(TryExtractAssistantContentText(assistant_message, &assistant_content));
EXPECT_EQ("first\nsecond\nthird", assistant_content);
}
TEST(QueryProfileAiAnalysisHelpersTest, ExtractAssistantContentHandlesStringContent) {
rapidjson::Document response_doc;
response_doc.Parse(R"json({"role":"assistant","content":"plain content"})json");
ASSERT_FALSE(response_doc.HasParseError());
const rapidjson::Value* assistant_message =
FindAssistantMessageInResponse(response_doc);
ASSERT_NE(nullptr, assistant_message);
string assistant_content;
EXPECT_TRUE(TryExtractAssistantContentText(assistant_message, &assistant_content));
EXPECT_EQ("plain content", assistant_content);
}
TEST(QueryProfileAiAnalysisHelpersTest,
ExtractAssistantContentRejectsNullOrAbnormalContent) {
rapidjson::Document assistant_message;
assistant_message.SetObject();
rapidjson::Document::AllocatorType& alloc = assistant_message.GetAllocator();
rapidjson::Value null_content;
null_content.SetNull();
assistant_message.AddMember("content", null_content, alloc);
string assistant_content = "previous";
EXPECT_FALSE(TryExtractAssistantContentText(&assistant_message, &assistant_content));
EXPECT_TRUE(assistant_content.empty());
rapidjson::Document object_content_message;
object_content_message.SetObject();
rapidjson::Value object_content(rapidjson::kObjectType);
object_content.AddMember("unexpected", 1, object_content_message.GetAllocator());
object_content_message.AddMember(
"content", object_content, object_content_message.GetAllocator());
assistant_content = "previous";
EXPECT_FALSE(TryExtractAssistantContentText(
&object_content_message, &assistant_content));
EXPECT_TRUE(assistant_content.empty());
}
TEST(QueryProfileAiAnalysisHelpersTest, FindAssistantMessageRejectsAbnormalEnvelope) {
rapidjson::Document response_doc;
response_doc.Parse(R"json({"choices":[{"message":"not-an-object"}]})json");
ASSERT_FALSE(response_doc.HasParseError());
const rapidjson::Value* assistant_message =
FindAssistantMessageInResponse(response_doc);
EXPECT_EQ(nullptr, assistant_message);
}
TEST(QueryProfileAiAnalysisHelpersTest, MaxConcurrentSlotsMultithreaded) {
const auto scoped_max_concurrent_runs = ScopedFlagSetter<int32_t>::Make(
&FLAGS_query_profile_ai_analysis_max_concurrent_runs, 4);
constexpr int num_threads = 10;
std::atomic<int> successful_acquires(0);
vector<std::thread> threads;
threads.reserve(num_threads);
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back([&successful_acquires]() {
if (TryAcquireAiAnalysisSlot()) {
successful_acquires.fetch_add(1, std::memory_order_relaxed);
}
});
}
for (auto& thread : threads) thread.join();
EXPECT_EQ(FLAGS_query_profile_ai_analysis_max_concurrent_runs,
successful_acquires.load());
EXPECT_FALSE(TryAcquireAiAnalysisSlot());
for (int i = 0; i < successful_acquires.load(); ++i) ReleaseAiAnalysisSlot();
}
TEST(QueryProfileAiAnalysisDeathTest, ReleaseSlotCannotDropBelowZero) {
GTEST_FLAG_SET(death_test_style, "threadsafe");
constexpr int32_t kMaxConcurrentRuns = 1;
const auto scoped_max_concurrent_runs = ScopedFlagSetter<int32_t>::Make(
&FLAGS_query_profile_ai_analysis_max_concurrent_runs, kMaxConcurrentRuns);
ASSERT_TRUE(TryAcquireAiAnalysisSlot());
ReleaseAiAnalysisSlot();
IMPALA_ASSERT_DEBUG_DEATH(
ReleaseAiAnalysisSlot(), "impala::ReleaseAiAnalysisSlot\\(\\)");
}
} // namespace
} // namespace impala