Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Measure manager fixups and improvements #5304

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions python/SetupPython.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ else()
endif()
endif()

execute_process(COMMAND ${Python_EXECUTABLE} -c "import requests; print(requests.__version__)"
RESULT_VARIABLE _PyRequests_STATUS
OUTPUT_VARIABLE PyRequests_Version
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(_PyRequests_STATUS AND NOT _PyRequests_STATUS EQUAL 0)
message(AUTHOR_WARNING "requests isn't installed on your system python, so some tests won't be run. Run `pip install requests`")
set(PyRequests_AVAILABLE OFF)
else()
message("Found Python requests: ${PyRequests_Version}")
set(PyRequests_AVAILABLE ON)
endif()
Comment on lines +38 to +50
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detect if requests is installed on the system python


get_filename_component(Python_PROGRAM_NAME ${Python_EXECUTABLE} NAME)

get_filename_component(RESOLVED_PYTHON_LIBRARY "${Python_LIBRARIES}" REALPATH)
Expand Down
14 changes: 7 additions & 7 deletions ruby/engine/measure_manager_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def do_POST (request, response)

result = {}

data = JSON.parse(request.body, {:symbolize_names=>true})
data = JSON.parse(request.body || "{}", {:symbolize_names=>true})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor tweaks in ruby CLI. When body is empty, you don't want to get bad error on JSON.parse with an ugly backtrace and No implicit conversion of nil into String isn't a good error message

my_measures_dir = data[:my_measures_dir]

if my_measures_dir
Expand All @@ -132,7 +132,7 @@ def do_POST (request, response)

result = []

data = JSON.parse(request.body, {:symbolize_names=>true})
data = JSON.parse(request.body || "{}", {:symbolize_names=>true})
uid = data[:uid]

if uid
Expand All @@ -156,8 +156,8 @@ def do_POST (request, response)

result = []

data = JSON.parse(request.body, {:symbolize_names=>true})
force_reload = false
data = JSON.parse(request.body || "{}", {:symbolize_names=>true})

# loop over all local BCL measures
OpenStudio::LocalBCL.instance.measures.each do |local_measure|
Expand All @@ -181,7 +181,7 @@ def do_POST (request, response)

result = []

data = JSON.parse(request.body, {:symbolize_names=>true})
data = JSON.parse(request.body || "{}", {:symbolize_names=>true})
measures_dir = data[:measures_dir] ? data[:measures_dir] : @my_measures_dir
force_reload = data[:force_reload] ? data[:force_reload] : false

Expand All @@ -205,7 +205,7 @@ def do_POST (request, response)

when "/compute_arguments"

data = JSON.parse(request.body, {:symbolize_names=>true})
data = JSON.parse(request.body || "{}", {:symbolize_names=>true})
measure_dir = data[:measure_dir ]
osm_path = data[:osm_path]
force_reload = data[:force_reload] ? data[:force_reload] : false
Expand Down Expand Up @@ -239,7 +239,7 @@ def do_POST (request, response)

when "/create_measure"

data = JSON.parse(request.body, {:symbolize_names=>true})
data = JSON.parse(request.body || "{}", {:symbolize_names=>true})
measure_dir = data[:measure_dir]

# name = data[:name] # we do not take name as input
Expand All @@ -263,7 +263,7 @@ def do_POST (request, response)

when "/duplicate_measure"

data = JSON.parse(request.body, {:symbolize_names=>true})
data = JSON.parse(request.body || "{}", {:symbolize_names=>true})
old_measure_dir = data[:old_measure_dir]
measure_dir = data[:measure_dir]

Expand Down
10 changes: 10 additions & 0 deletions src/cli/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,16 @@ if(BUILD_TESTING)
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/Testing/"
)

if (PyRequests_AVAILABLE)
add_test(NAME OpenStudioCLI.test_measure_manager
COMMAND ${Python_EXECUTABLE} -m pytest --verbose --os-cli-path $<TARGET_FILE:openstudio> "${CMAKE_CURRENT_SOURCE_DIR}/test/test_measure_manager.py"
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/Testing/"
)
add_test(NAME OpenStudioCLI.Classic.test_measure_manager
COMMAND ${Python_EXECUTABLE} -m pytest --verbose --use-classic --os-cli-path $<TARGET_FILE:openstudio> "${CMAKE_CURRENT_SOURCE_DIR}/test/test_measure_manager.py"
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/Testing/"
)
endif()
Comment on lines +367 to +376
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Register the Classic (Ruby) and C++ version of the test_measure_manager.py


else()
# TODO: Remove. Fallback on these for now, as I don't know if CI has pytest installed
Expand Down
49 changes: 39 additions & 10 deletions src/cli/MeasureManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "../utilities/core/Filesystem.hpp"
#include "../utilities/core/FilesystemHelpers.hpp"
#include "../utilities/core/StringHelpers.hpp"
#include "../utilities/time/DateTime.hpp"
#include "../osversion/VersionTranslator.hpp"
#include "../energyplus/ForwardTranslator.hpp"
#include "../utilities/bcl/LocalBCL.hpp"
Expand Down Expand Up @@ -140,7 +141,7 @@ Json::Value MeasureManager::internalState() const {
osmInfo["checksum"] = v.checksum;
osms.append(std::move(osmInfo));
}
result["osm"] = std::move(osms);
result["osms"] = std::move(osms);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to match Ruby


Json::Value idfs(Json::arrayValue);
for (const auto& [k, v] : m_idfs) {
Expand All @@ -149,15 +150,18 @@ Json::Value MeasureManager::internalState() const {
idfInfo["checksum"] = v.checksum;
idfs.append(std::move(idfInfo));
}
result["idf"] = std::move(idfs);
result["idfs"] = std::move(idfs);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ruby didn't have this. But since I use osms now, makes sense to use idfs


// Json::Value measures(Json::arrayValue);
auto& measures = result["measures"];
measures = Json::arrayValue;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it "[]" and not None

for (const auto& [measureDirPath, bclMeasureInfo] : m_measures) {
Json::Value mInfo(Json::objectValue);
measures.append(bclMeasureInfo.measure.toJSON());
}

auto& measureInfos = result["measure_info"];
measureInfos = Json::arrayValue;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

// Json::Value measureInfos(Json::arrayValue);
for (const auto& [measureDirPath, bclMeasureInfo] : m_measures) {
for (const auto& [osmOrIdfPath, measureInfo] : bclMeasureInfo.measureInfos) {
Expand Down Expand Up @@ -592,14 +596,17 @@ bool MeasureManagerServer::close() {
return status == pplx::task_group_status::completed;
}

void MeasureManagerServer::unknown_endpoint(web::http::http_request& message) {
const std::string uri = toString(web::http::uri::decode(message.relative_uri().path()));
message.reply(web::http::status_codes::BadRequest, toWebJSON(fmt::format("Error, unknown path '{}'", uri)));
print_feedback(message, web::http::status_codes::NotFound);
}
Comment on lines +599 to +603
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a function for both GET and POST when 404


void MeasureManagerServer::handle_get(web::http::http_request message) {
const std::string uri = toString(web::http::uri::decode(message.relative_uri().path()));

if (uri == "/") {
Json::Value result;
result["status"] = "running";
result["my_measures_dir"] = my_measures_dir.generic_string();
message.reply(web::http::status_codes::OK, toWebJSON(result));
handle_request(message, web::json::value(), &MeasureManagerServer::status);
return;
}

Expand All @@ -609,7 +616,7 @@ void MeasureManagerServer::handle_get(web::http::http_request message) {
return;
}

message.reply(web::http::status_codes::BadRequest, toWebJSON(fmt::format("Error, unknown path '{}'", uri)));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Get, was returning a 400 instead of a 404 when unknown endpoint

unknown_endpoint(message);
}

void MeasureManagerServer::handle_post(web::http::http_request message) {
Expand Down Expand Up @@ -667,8 +674,16 @@ void MeasureManagerServer::handle_post(web::http::http_request message) {
return;
}

message.reply(web::http::status_codes::NotFound, toWebJSON("404: Unknown Endpoint"));
unknown_endpoint(message);
}

MeasureManagerServer::ResponseType MeasureManagerServer::status([[maybe_unused]] const web::json::value& body) {
Json::Value result;
result["status"] = "running";
result["my_measures_dir"] = my_measures_dir.generic_string();
return {web::http::status_codes::OK, toWebJSON(result)};
}

MeasureManagerServer::ResponseType MeasureManagerServer::internal_state([[maybe_unused]] const web::json::value& body) {
Json::Value result;
result["status"] = "running";
Expand Down Expand Up @@ -984,30 +999,44 @@ MeasureManagerServer::ResponseType MeasureManagerServer::duplicate_measure(const
}
}

void MeasureManagerServer::print_feedback(const web::http::http_request& message, web::http::status_code status_code) {
const std::string uri = toString(web::http::uri::decode(message.relative_uri().path()));
const std::string method = toString(message.method());
const std::string http_version = message.http_version().to_utf8string();
const std::string timestamp = openstudio::DateTime::now().toXsdDateTime();
fmt::print(status_code == web::http::status_codes::OK ? stdout : stderr, "[{}] \"{} {} {}\" {}\n", openstudio::DateTime::now().toXsdDateTime(),
method, uri, http_version, status_code);
}
Comment on lines +1002 to +1009
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Print log


void MeasureManagerServer::handle_request(const web::http::http_request& message, const web::json::value& body,
memRequestHandlerFunPtr request_handler) {

std::packaged_task<ResponseType()> task([this, &body, &request_handler]() { return (this->*request_handler)(body); });

auto future_result = task.get_future(); // The task hasn't been started yet
tasks.push_back(std::move(task)); // It gets queued, the **main** thread will process it
web::http::status_code status_code = web::http::status_codes::Created;
try {
auto result = future_result.get(); // This block until it's been processed
message.reply(result.status_code, result.body);
status_code = result.status_code;
message.reply(status_code, result.body);
} catch (const std::exception& e) {
constexpr auto msg = "MeasureManager Server encountered an error:\n\"{}\"\n";
fmt::print(msg, e.what());
status_code = web::http::status_codes::InternalError;
message.reply(web::http::status_codes::InternalError, fmt::format(msg, e.what()));
}
print_feedback(message, status_code);
}

void MeasureManagerServer::do_tasks_forever() {
fmt::print("MeasureManager Ready");
fmt::print("MeasureManager Ready\n");
fmt::print("Accepting requests on: {}\n", m_url);
std::fflush(stdout);
while (true) {
auto task = tasks.wait_for_one();
task();
std::fflush(stdout);
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/cli/MeasureManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class MeasureManagerServer
};

// Request handlers
ResponseType status(const web::json::value& body);
ResponseType internal_state(const web::json::value& body);
ResponseType reset(const web::json::value& body);
ResponseType set(const web::json::value& body);
Expand All @@ -129,6 +130,15 @@ class MeasureManagerServer
void handle_get(web::http::http_request message);
void handle_post(web::http::http_request message);
static void handle_error(pplx::task<void>& t);

// Helper to return a 404 error
static void unknown_endpoint(web::http::http_request& message);

// Print the request to the console (stdout if Ok, stderr otherwise)
// [2024-11-14T10:21:46+01:00] "POST /reset HTTP/1.1" 200
// [2024-11-14T10:22:09+01:00] "GET /dsd HTTP/1.1" 400
static void print_feedback(const web::http::http_request& message, web::http::status_code status_code);

MeasureManager m_measureManager;
web::http::experimental::listener::http_listener m_listener;
ThreadSafeDeque<std::packaged_task<ResponseType()>> tasks;
Expand Down
7 changes: 6 additions & 1 deletion src/cli/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ def validate_file(arg):

def pytest_addoption(parser):
parser.addoption("--os-cli-path", type=validate_file, help="Path to the OS CLI") # , required=True

parser.addoption("--use-classic", action="store_true", help="Force use the Classic CLI")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need addopt


@pytest.fixture(scope="module")
def osclipath(request):
cli_path = request.config.getoption("--os-cli-path")
if cli_path is None:
raise ValueError("You must supply --os-cli-path [Path]")
return cli_path

@pytest.fixture(scope="module")
def use_classic_cli(request):
use_classic = request.config.getoption("--use-classic")
return use_classic
Comment on lines +23 to +27
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set it as a fixture for use

Loading
Loading