mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-23 19:02:10 +00:00
feat(automation): add file.open dispatcher handler with validation + tests
This commit is contained in:
@@ -127,6 +127,33 @@ std::vector<KeyChord> parse_keys(const nlohmann::json& params) {
|
|||||||
throw AutomationError(kInvalidParams, "'keys' is empty");
|
throw AutomationError(kInvalidParams, "'keys' is empty");
|
||||||
return { chord_from_tokens(tokens) };
|
return { chord_from_tokens(tokens) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "paths" may be a single string ("C:/a.stl") or an array of strings. Returns the
|
||||||
|
// non-empty absolute paths; throws kInvalidParams when paths is missing, not a
|
||||||
|
// string/array, contains a non-string entry, or yields no non-empty path.
|
||||||
|
std::vector<std::string> parse_paths(const nlohmann::json& params) {
|
||||||
|
if (!params.is_object() || !params.contains("paths"))
|
||||||
|
throw AutomationError(kInvalidParams, "file.open requires 'paths'");
|
||||||
|
const auto& p = params.at("paths");
|
||||||
|
std::vector<std::string> out;
|
||||||
|
if (p.is_string()) {
|
||||||
|
out.push_back(p.get<std::string>());
|
||||||
|
} else if (p.is_array()) {
|
||||||
|
for (const auto& e : p) {
|
||||||
|
if (!e.is_string())
|
||||||
|
throw AutomationError(kInvalidParams, "'paths' entries must be strings");
|
||||||
|
out.push_back(e.get<std::string>());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw AutomationError(kInvalidParams, "'paths' must be a string or array");
|
||||||
|
}
|
||||||
|
out.erase(std::remove_if(out.begin(), out.end(),
|
||||||
|
[](const std::string& s) { return s.empty(); }),
|
||||||
|
out.end());
|
||||||
|
if (out.empty())
|
||||||
|
throw AutomationError(kInvalidParams, "'paths' is empty");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -193,6 +220,7 @@ nlohmann::json JsonRpcDispatcher::dispatch(const nlohmann::json& request) {
|
|||||||
if (method == "sync.wait_for") return make_result(id, m_sync_wait_for(params));
|
if (method == "sync.wait_for") return make_result(id, m_sync_wait_for(params));
|
||||||
if (method == "app.state") return make_result(id, m_app_state(params));
|
if (method == "app.state") return make_result(id, m_app_state(params));
|
||||||
if (method == "screenshot.window") return make_result(id, m_screenshot_window(params));
|
if (method == "screenshot.window") return make_result(id, m_screenshot_window(params));
|
||||||
|
if (method == "file.open") return make_result(id, m_file_open(params));
|
||||||
return make_error(id, kMethodNotFound, "unknown method: " + method);
|
return make_error(id, kMethodNotFound, "unknown method: " + method);
|
||||||
} catch (const AutomationError& e) {
|
} catch (const AutomationError& e) {
|
||||||
return make_error(id, e.code, e.what());
|
return make_error(id, e.code, e.what());
|
||||||
@@ -342,4 +370,10 @@ nlohmann::json JsonRpcDispatcher::m_screenshot_window(const nlohmann::json& para
|
|||||||
return image_to_json(m_backend.screenshot_window(target_ptr));
|
return image_to_json(m_backend.screenshot_window(target_ptr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nlohmann::json JsonRpcDispatcher::m_file_open(const nlohmann::json& params) {
|
||||||
|
const std::vector<std::string> paths = parse_paths(params);
|
||||||
|
const int loaded = m_backend.open_files(paths);
|
||||||
|
return { {"ok", true}, {"loaded", loaded} };
|
||||||
|
}
|
||||||
|
|
||||||
}}} // namespace
|
}}} // namespace
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ private:
|
|||||||
nlohmann::json m_sync_wait_for(const nlohmann::json& params);
|
nlohmann::json m_sync_wait_for(const nlohmann::json& params);
|
||||||
nlohmann::json m_app_state(const nlohmann::json& params);
|
nlohmann::json m_app_state(const nlohmann::json& params);
|
||||||
nlohmann::json m_screenshot_window(const nlohmann::json& params);
|
nlohmann::json m_screenshot_window(const nlohmann::json& params);
|
||||||
|
nlohmann::json m_file_open(const nlohmann::json& params);
|
||||||
|
|
||||||
// Resolve a unique, actionable (enabled+visible) node from params["target"].
|
// Resolve a unique, actionable (enabled+visible) node from params["target"].
|
||||||
// Throws kErrNotFound (missing/ambiguous) or kErrNotActionable (disabled/hidden).
|
// Throws kErrNotFound (missing/ambiguous) or kErrNotActionable (disabled/hidden).
|
||||||
|
|||||||
@@ -195,3 +195,65 @@ TEST_CASE("sync.wait_for times out -> 1003", "[automation][rpc]") {
|
|||||||
{"timeout_ms",30},{"poll_ms",5}}}});
|
{"timeout_ms",30},{"poll_ms",5}}}});
|
||||||
CHECK(resp.at("error").at("code") == kErrWaitTimeout);
|
CHECK(resp.at("error").at("code") == kErrWaitTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("file.open with an array of paths routes to backend", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock;
|
||||||
|
mock.open_return_count = 3;
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",1},{"method","file.open"},
|
||||||
|
{"params",{{"paths", json::array({"C:/abs/a.stl","C:/abs/b.stl"})}}}});
|
||||||
|
CHECK(resp.at("result").at("ok") == true);
|
||||||
|
CHECK(resp.at("result").at("loaded") == 3);
|
||||||
|
REQUIRE(mock.opened_paths.size() == 1);
|
||||||
|
REQUIRE(mock.opened_paths[0].size() == 2);
|
||||||
|
CHECK(mock.opened_paths[0][0] == "C:/abs/a.stl");
|
||||||
|
CHECK(mock.opened_paths[0][1] == "C:/abs/b.stl");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("file.open accepts a bare string path", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock;
|
||||||
|
mock.open_return_count = 1;
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",2},{"method","file.open"},
|
||||||
|
{"params",{{"paths","C:/abs/a.stl"}}}});
|
||||||
|
CHECK(resp.at("result").at("loaded") == 1);
|
||||||
|
REQUIRE(mock.opened_paths.size() == 1);
|
||||||
|
REQUIRE(mock.opened_paths[0].size() == 1);
|
||||||
|
CHECK(mock.opened_paths[0][0] == "C:/abs/a.stl");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("file.open with missing paths -> invalid params", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock;
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",3},{"method","file.open"},
|
||||||
|
{"params", json::object()}});
|
||||||
|
CHECK(resp.at("error").at("code") == kInvalidParams);
|
||||||
|
CHECK(mock.opened_paths.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("file.open with empty paths array -> invalid params", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock;
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",4},{"method","file.open"},
|
||||||
|
{"params",{{"paths", json::array()}}}});
|
||||||
|
CHECK(resp.at("error").at("code") == kInvalidParams);
|
||||||
|
CHECK(mock.opened_paths.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("file.open with a non-string entry -> invalid params", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock;
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",5},{"method","file.open"},
|
||||||
|
{"params",{{"paths", json::array({"C:/a.stl", 42})}}}});
|
||||||
|
CHECK(resp.at("error").at("code") == kInvalidParams);
|
||||||
|
CHECK(mock.opened_paths.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("file.open backend load failure -> 1007", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock;
|
||||||
|
mock.open_should_fail = true;
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",6},{"method","file.open"},
|
||||||
|
{"params",{{"paths","C:/abs/a.stl"}}}});
|
||||||
|
CHECK(resp.at("error").at("code") == kErrLoadFailed);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user