diff options
author | Håvard Pettersen <havardpe@oath.com> | 2021-08-25 13:38:20 +0000 |
---|---|---|
committer | Håvard Pettersen <havardpe@oath.com> | 2021-08-30 11:15:57 +0000 |
commit | 5b4b8cfc34e7a61971ef43d707446d16ff101d03 (patch) | |
tree | 56380802eaf0f4567868979d0ff722edf830d044 /eval | |
parent | 2a679a9b0ef339c9b417eacef6b2d31a864c31d9 (diff) |
add json-repl (and interactive) modes to vespa-eval-expr
Diffstat (limited to 'eval')
-rw-r--r-- | eval/CMakeLists.txt | 1 | ||||
-rw-r--r-- | eval/src/apps/eval_expr/eval_expr.cpp | 300 | ||||
-rw-r--r-- | eval/src/tests/apps/eval_expr/CMakeLists.txt | 9 | ||||
-rw-r--r-- | eval/src/tests/apps/eval_expr/eval_expr_test.cpp | 265 | ||||
-rw-r--r-- | eval/src/vespa/eval/eval/tensor_spec.cpp | 3 | ||||
-rw-r--r-- | eval/src/vespa/eval/eval/test/test_io.cpp | 33 | ||||
-rw-r--r-- | eval/src/vespa/eval/eval/test/test_io.h | 17 |
7 files changed, 576 insertions, 52 deletions
diff --git a/eval/CMakeLists.txt b/eval/CMakeLists.txt index 4b2127d8a3a..16dd729b1f1 100644 --- a/eval/CMakeLists.txt +++ b/eval/CMakeLists.txt @@ -12,6 +12,7 @@ vespa_define_module( TESTS src/tests/ann + src/tests/apps/eval_expr src/tests/eval/addr_to_symbol src/tests/eval/aggr src/tests/eval/array_array_map diff --git a/eval/src/apps/eval_expr/eval_expr.cpp b/eval/src/apps/eval_expr/eval_expr.cpp index d6c95772498..a5dd5368d28 100644 --- a/eval/src/apps/eval_expr/eval_expr.cpp +++ b/eval/src/apps/eval_expr/eval_expr.cpp @@ -1,5 +1,6 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/vespalib/util/require.h> #include <vespa/eval/eval/function.h> #include <vespa/eval/eval/tensor_spec.h> #include <vespa/eval/eval/value_type.h> @@ -19,94 +20,274 @@ using vespalib::make_string_short::fmt; using namespace vespalib::eval; +using namespace vespalib::eval::test; + +using vespalib::Slime; +using vespalib::slime::JsonFormat; +using vespalib::slime::Inspector; +using vespalib::slime::Cursor; + const auto &factory = FastValueBuilderFactory::get(); int usage(const char *self) { + // ------------------------------------------------------------------------------- fprintf(stderr, "usage: %s [--verbose] <expr> [expr ...]\n", self); fprintf(stderr, " Evaluate a sequence of expressions. The first expression must be\n"); fprintf(stderr, " self-contained (no external values). Later expressions may use the\n"); fprintf(stderr, " results of earlier expressions. Expressions are automatically named\n"); fprintf(stderr, " using single letter symbols ('a' through 'z'). Quote expressions to\n"); - fprintf(stderr, " make sure they become separate parameters.\n"); - fprintf(stderr, " The --verbose option may be specified to get more detailed informaion\n"); - fprintf(stderr, " about how the various expressions are optimized.\n"); + fprintf(stderr, " make sure they become separate parameters. The --verbose option may\n"); + fprintf(stderr, " be specified to get more detailed informaion about how the various\n"); + fprintf(stderr, " expressions are optimized.\n\n"); fprintf(stderr, "example: %s \"2+2\" \"a+2\" \"a+b\"\n", self); - fprintf(stderr, " (a=4, b=6, c=10)\n\n"); + fprintf(stderr, " (a=4, b=6, c=10)\n"); + fprintf(stderr, "\n"); + fprintf(stderr, "advanced usage: %s interactive\n", self); + fprintf(stderr, " This runs the progam in interactive mode. possible commands (line based):\n"); + fprintf(stderr, " 'exit' -> exit the program\n"); + fprintf(stderr, " 'verbose (true|false)' -> enable or disable verbose output\n"); + fprintf(stderr, " 'def <name> <expr>' -> evaluate expression, bind result to a name\n"); + fprintf(stderr, " '<expr>' -> evaluate expression\n"); + fprintf(stderr, "\n"); + fprintf(stderr, "advanced usage: %s json-repl\n", self); + fprintf(stderr, " This will put the program into a read-eval-print loop where it reads\n"); + fprintf(stderr, " json objects from stdin and writes json objects to stdout.\n"); + fprintf(stderr, " possible commands: (object based)\n"); + fprintf(stderr, " {expr:<expr>, ?name:<name>, ?verbose:true}\n"); + fprintf(stderr, " -> { result:<verbatim-expr> ?steps:[{class:string,symbol:string}] }\n"); + fprintf(stderr, " Evaluate an expression and return the result. If a name is specified,\n"); + fprintf(stderr, " the result will be bound to that name and will be available as a symbol\n"); + fprintf(stderr, " when doing future evaluations. Verbose output must be enabled for each\n"); + fprintf(stderr, " relevant command and will result in the 'steps' field being populated in\n"); + fprintf(stderr, " the response.\n"); + fprintf(stderr, " if any command fails, the response will be { error:string }\n"); + fprintf(stderr, "\n"); + // ------------------------------------------------------------------------------- return 1; } + int overflow(int cnt, int max) { fprintf(stderr, "error: too many expressions: %d (max is %d)\n", cnt, max); return 2; } -struct Context { - std::vector<vespalib::string> param_names; - std::vector<ValueType> param_types; - std::vector<Value::UP> param_values; - std::vector<Value::CREF> param_refs; - bool collect_meta; - CTFMetaData meta; +class Context +{ +private: + std::vector<vespalib::string> _param_names; + std::vector<ValueType> _param_types; + std::vector<Value::UP> _param_values; + std::vector<Value::CREF> _param_refs; + bool _verbose; + vespalib::string _error; + CTFMetaData _meta; + + void clear_state() { + _error.clear(); + _meta = CTFMetaData(); + } - Context(bool collect_meta_in) : param_names(), param_types(), param_values(), param_refs(), collect_meta(collect_meta_in), meta() {} +public: + Context() : _param_names(), _param_types(), _param_values(), _param_refs(), _verbose(), _meta() {} ~Context(); - bool eval_next(const vespalib::string &name, const vespalib::string &expr) { - meta = CTFMetaData(); - SimpleObjectParams params(param_refs); - auto fun = Function::parse(param_names, expr, FeatureNameExtractor()); + void verbose(bool value) { _verbose = value; } + bool verbose() const { return _verbose; } + + Value::UP eval(const vespalib::string &expr) { + clear_state(); + SimpleObjectParams params(_param_refs); + auto fun = Function::parse(_param_names, expr, FeatureNameExtractor()); if (fun->has_error()) { - fprintf(stderr, "error: expression parse error (%s): %s\n", name.c_str(), fun->get_error().c_str()); - return false; + _error = fmt("expression parsing failed: %s", fun->get_error().c_str()); + return {}; } - NodeTypes types = NodeTypes(*fun, param_types); + NodeTypes types = NodeTypes(*fun, _param_types); ValueType res_type = types.get_type(fun->root()); if (res_type.is_error() || !types.errors().empty()) { - fprintf(stderr, "error: expression type issues (%s)\n", name.c_str()); + _error = fmt("type resolving failed for expression: '%s'", expr.c_str()); for (const auto &issue: types.errors()) { - fprintf(stderr, " issue: %s\n", issue.c_str()); + _error.append(fmt("\n type issue: %s", issue.c_str())); } - return false; + return {}; } vespalib::Stash stash; const TensorFunction &plain_fun = make_tensor_function(factory, fun->root(), types, stash); const TensorFunction &optimized = optimize_tensor_function(factory, plain_fun, stash); - InterpretedFunction ifun(factory, optimized, collect_meta ? &meta : nullptr); + InterpretedFunction ifun(factory, optimized, _verbose ? &_meta : nullptr); InterpretedFunction::Context ctx(ifun); - Value::UP result = factory.copy(ifun.eval(ctx, params)); - assert(result->type() == res_type); - param_names.push_back(name); - param_types.push_back(res_type); - param_values.push_back(std::move(result)); - param_refs.emplace_back(*param_values.back()); - return true; - } - - void print_last(bool with_name) { - auto spec = spec_from_value(param_refs.back().get()); - if (!meta.steps.empty()) { - if (with_name) { - fprintf(stderr, "meta-data(%s):\n", param_names.back().c_str()); + auto result = factory.copy(ifun.eval(ctx, params)); + REQUIRE_EQ(result->type(), res_type); + return result; + } + + const vespalib::string &error() const { return _error; } + const CTFMetaData &meta() const { return _meta; } + + void save(const vespalib::string &name, Value::UP value) { + REQUIRE(value); + for (size_t i = 0; i < _param_names.size(); ++i) { + if (_param_names[i] == name) { + _param_types[i] = value->type(); + _param_values[i] = std::move(value); + _param_refs[i] = *_param_values[i]; + return; + } + } + _param_names.push_back(name); + _param_types.push_back(value->type()); + _param_values.push_back(std::move(value)); + _param_refs.emplace_back(*_param_values.back()); + } +}; +Context::~Context() = default; + +void print_error(const vespalib::string &error) { + fprintf(stderr, "error: %s\n", error.c_str()); +} + +void print_value(const Value &value, const vespalib::string &name, const CTFMetaData &meta) { + bool with_name = !name.empty(); + bool with_meta = !meta.steps.empty(); + auto spec = spec_from_value(value); + if (with_meta) { + if (with_name) { + fprintf(stderr, "meta-data(%s):\n", name.c_str()); + } else { + fprintf(stderr, "meta-data:\n"); + } + for (const auto &step: meta.steps) { + fprintf(stderr, " class: %s\n", step.class_name.c_str()); + fprintf(stderr, " symbol: %s\n", step.symbol_name.c_str()); + } + } + if (with_name) { + fprintf(stdout, "%s: ", name.c_str()); + } + if (value.type().is_double()) { + fprintf(stdout, "%.32g\n", spec.as_double()); + } else { + fprintf(stdout, "%s\n", spec.to_string().c_str()); + } +} + +void handle_message(Context &ctx, const Inspector &req, Cursor &reply) { + vespalib::string expr = req["expr"].asString().make_string(); + vespalib::string name = req["name"].asString().make_string(); + ctx.verbose(req["verbose"].asBool()); + if (expr.empty()) { + reply.setString("error", "missing expression (field name: 'expr')"); + return; + } + auto value = ctx.eval(expr); + if (!value) { + reply.setString("error", ctx.error()); + return; + } + reply.setString("result", spec_from_value(*value).to_expr()); + if (!name.empty()) { + ctx.save(name, std::move(value)); + } + if (!ctx.meta().steps.empty()) { + auto &steps_out = reply.setArray("steps"); + for (const auto &step: ctx.meta().steps) { + auto &step_out = steps_out.addObject(); + step_out.setString("class", step.class_name); + step_out.setString("symbol", step.symbol_name); + } + } +} + +const vespalib::string exit_cmd("exit"); +const vespalib::string verbose_cmd("verbose "); +const vespalib::string def_cmd("def "); + +bool is_only_whitespace(const vespalib::string &str) { + for (auto c: str) { + if (!isspace(c)) { + return false; + } + } + return true; +} + +int interactive_mode(Context &ctx) { + StdIn std_in; + LineReader input(std_in); + vespalib::string line; + while (input.read_line(line)) { + if (is_only_whitespace(line)) { + continue; + } + if (line.find(exit_cmd) == 0) { + return 0; + } + if (line.find(verbose_cmd) == 0) { + auto flag_str = line.substr(verbose_cmd.size()); + bool flag = (flag_str == "true"); + bool bad = (!flag && (flag_str != "false")); + if (bad) { + fprintf(stderr, "bad flag specifier: '%s', must be 'true' or 'false'\n", flag_str.c_str()); } else { - fprintf(stderr, "meta-data:\n"); + ctx.verbose(flag); + fprintf(stdout, "verbose set to %s\n", flag ? "true" : "false"); } - for (const auto &step: meta.steps) { - fprintf(stderr, " class: %s\n", step.class_name.c_str()); - fprintf(stderr, " symbol: %s\n", step.symbol_name.c_str()); + continue; + } + vespalib::string expr; + vespalib::string name; + if (line.find(def_cmd) == 0) { + auto name_size = (line.find(" ", def_cmd.size()) - def_cmd.size()); + name = line.substr(def_cmd.size(), name_size); + expr = line.substr(def_cmd.size() + name_size + 1); + } else { + expr = line; + } + if (ctx.verbose()) { + fprintf(stderr, "eval '%s'", expr.c_str()); + if (name.empty()) { + fprintf(stderr, "\n"); + } else { + fprintf(stderr, " -> '%s'\n", name.c_str()); } } - if (with_name) { - fprintf(stdout, "%s: ", param_names.back().c_str()); + if (auto value = ctx.eval(expr)) { + print_value(*value, name, ctx.meta()); + if (!name.empty()) { + ctx.save(name, std::move(value)); + } + } else { + print_error(ctx.error()); } - if (param_types.back().is_double()) { - fprintf(stdout, "%.32g\n", spec.as_double()); + } + return 0; +} + +int json_repl_mode(Context &ctx) { + StdIn std_in; + StdOut std_out; + for (;;) { + if (look_for_eof(std_in)) { + return 0; + } + Slime req; + if (!JsonFormat::decode(std_in, req)) { + return 3; + } + Slime reply; + if (req.get().type().getId() == vespalib::slime::ARRAY::ID) { + reply.setArray(); + for (size_t i = 0; i < req.get().entries(); ++i) { + handle_message(ctx, req[i], reply.get().addObject()); + } } else { - fprintf(stdout, "%s\n", spec.to_string().c_str()); + handle_message(ctx, req.get(), reply.setObject()); } + write_compact(reply, std_out); } -}; -Context::~Context() = default; +} int main(int argc, char **argv) { bool verbose = ((argc > 1) && (vespalib::string(argv[1]) == "--verbose")); @@ -119,14 +300,29 @@ int main(int argc, char **argv) { if (expr_cnt > expr_max) { return overflow(expr_cnt, expr_max); } - Context ctx(verbose); + Context ctx; + if ((expr_cnt == 1) && (vespalib::string(argv[expr_idx]) == "interactive")) { + return interactive_mode(ctx); + } + if ((expr_cnt == 1) && (vespalib::string(argv[expr_idx]) == "json-repl")) { + return json_repl_mode(ctx); + } + ctx.verbose(verbose); vespalib::string name("a"); for (int i = expr_idx; i < argc; ++i) { - if (!ctx.eval_next(name, argv[i])) { + if (auto value = ctx.eval(argv[i])) { + if (expr_cnt > 1) { + print_value(*value, name, ctx.meta()); + ctx.save(name, std::move(value)); + ++name[0]; + } else { + vespalib::string no_name; + print_value(*value, no_name, ctx.meta()); + } + } else { + print_error(ctx.error()); return 3; } - ctx.print_last(expr_cnt > 1); - ++name[0]; } return 0; } diff --git a/eval/src/tests/apps/eval_expr/CMakeLists.txt b/eval/src/tests/apps/eval_expr/CMakeLists.txt new file mode 100644 index 00000000000..84f90f61f49 --- /dev/null +++ b/eval/src/tests/apps/eval_expr/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(eval_eval_expr_test_app TEST + SOURCES + eval_expr_test.cpp + DEPENDS + vespalib +) +vespa_add_test(NAME eval_eval_expr_test_app COMMAND eval_eval_expr_test_app + DEPENDS eval_eval_expr_test_app eval_eval_expr_app) diff --git a/eval/src/tests/apps/eval_expr/eval_expr_test.cpp b/eval/src/tests/apps/eval_expr/eval_expr_test.cpp new file mode 100644 index 00000000000..8e0b3286421 --- /dev/null +++ b/eval/src/tests/apps/eval_expr/eval_expr_test.cpp @@ -0,0 +1,265 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/vespalib/testkit/test_kit.h> +#include <vespa/vespalib/testkit/time_bomb.h> +#include <vespa/vespalib/util/stringfmt.h> +#include <vespa/vespalib/data/slime/slime.h> +#include <vespa/vespalib/util/child_process.h> +#include <vespa/vespalib/data/input.h> +#include <vespa/vespalib/data/output.h> +#include <vespa/vespalib/data/simple_buffer.h> +#include <vespa/vespalib/util/size_literals.h> + +using namespace vespalib; +using vespalib::make_string_short::fmt; +using vespalib::slime::JsonFormat; +using vespalib::slime::Inspector; + +vespalib::string module_build_path("../../../../"); +vespalib::string binary = module_build_path + "src/apps/eval_expr/vespa-eval-expr"; +vespalib::string server_cmd = binary + " --verbose json-repl"; + +//----------------------------------------------------------------------------- + +void read_until_eof(Input &input) { + for (auto mem = input.obtain(); mem.size > 0; mem = input.obtain()) { + input.evict(mem.size); + } +} + +// copied from vespalib::eval::test +// +// It seems that linking with the eval library contaminates the +// process proxy in such a way that valgrind will fail. The working +// theory is that some static state gets initialized before the proxy +// is forked. This state conflicts with itself when the eval library +// is loaded again when doing exec on vespa-eval-expr. This test +// bypasses the issue by linking with vespalib instead. A more robust +// solution would be to reverse the roles of the process proxy and the +// program; letting the proxy start the program. This could also be +// combined with the ability to send open file descriptors on unix +// domain sockets to avoid indirection for stdin/stdout streams. + +void write_compact(const Slime &slime, Output &out) { + JsonFormat::encode(slime, out, true); + out.reserve(1).data[0] = '\n'; + out.commit(1); +} + +// Output adapter used to write to stdin of a child process +class ChildIn : public Output { + ChildProcess &_child; + SimpleBuffer _output; +public: + ChildIn(ChildProcess &child) : _child(child) {} + WritableMemory reserve(size_t bytes) override { + return _output.reserve(bytes); + } + Output &commit(size_t bytes) override { + _output.commit(bytes); + Memory buf = _output.obtain(); + ASSERT_TRUE(_child.write(buf.data, buf.size)); + _output.evict(buf.size); + return *this; + } +}; + +// Input adapter used to read from stdout of a child process +class ChildOut : public Input { + ChildProcess &_child; + SimpleBuffer _input; +public: + ChildOut(ChildProcess &child) + : _child(child) + { + EXPECT_TRUE(_child.running()); + EXPECT_TRUE(!_child.failed()); + } + Memory obtain() override { + if ((_input.get().size == 0) && !_child.eof()) { + WritableMemory buf = _input.reserve(4_Ki); + uint32_t res = _child.read(buf.data, buf.size); + ASSERT_TRUE((res > 0) || _child.eof()); + _input.commit(res); + } + return _input.obtain(); + } + Input &evict(size_t bytes) override { + _input.evict(bytes); + return *this; + } +}; + +//----------------------------------------------------------------------------- + +struct Result { + vespalib::string error; + vespalib::string result; + std::vector<std::pair<vespalib::string, vespalib::string>> steps; + + Result(const Inspector &obj) + : error(obj["error"].asString().make_string()), + result(obj["result"].asString().make_string()), + steps() + { + const auto &arr = obj["steps"]; + for (size_t i = 0; i < arr.entries(); ++i) { + steps.emplace_back(arr[i]["class"].asString().make_string(), + arr[i]["symbol"].asString().make_string()); + } + } + void verify_result(const vespalib::string &expect) { + EXPECT_EQUAL(error, ""); + EXPECT_EQUAL(result, expect); + } + void verify_error(const vespalib::string &expect) { + EXPECT_EQUAL(steps.size(), 0u); + EXPECT_EQUAL(result, ""); + fprintf(stderr, "... does error '%s' contain message '%s'?\n", + error.c_str(), expect.c_str()); + EXPECT_TRUE(error.find(expect) != error.npos); + } + ~Result(); +}; +Result::~Result() = default; + +void dump_message(const char *prefix, const Slime &slime) { + SimpleBuffer buf; + slime::JsonFormat::encode(slime, buf, true); + auto str = buf.get().make_string(); + fprintf(stderr, "%s%s\n", prefix, str.c_str()); +} + +class Server { +private: + TimeBomb _bomb; + ChildProcess _child; + ChildIn _child_stdin; + ChildOut _child_stdout; +public: + Server() + : _bomb(60), + _child(server_cmd.c_str()), + _child_stdin(_child), + _child_stdout(_child) {} + ~Server(); + Slime invoke(const Slime &req) { + dump_message("request --> ", req); + write_compact(req, _child_stdin); + Slime reply; + ASSERT_TRUE(JsonFormat::decode(_child_stdout, reply)); + dump_message(" reply <-- ", reply); + return reply; + } + Result eval(const vespalib::string &expr, const vespalib::string &name = {}, bool verbose = false) { + Slime req; + auto &obj = req.setObject(); + obj.setString("expr", expr.c_str()); + if (!name.empty()) { + obj.setString("name", name.c_str()); + } + if (verbose) { + obj.setBool("verbose", true); + } + Slime reply = invoke(req); + return {reply.get()}; + } +}; +Server::~Server() { + _child.close(); + read_until_eof(_child_stdout); + ASSERT_TRUE(_child.wait()); + ASSERT_TRUE(!_child.running()); + ASSERT_TRUE(!_child.failed()); +} + +//----------------------------------------------------------------------------- + +TEST("print server command") { + fprintf(stderr, "server cmd: %s\n", server_cmd.c_str()); +} + +//----------------------------------------------------------------------------- + +TEST_F("require that simple evaluation works", Server()) { + TEST_DO(f1.eval("2+2").verify_result("4")); +} + +TEST_F("require that multiple dependent expressions work", Server()) { + TEST_DO(f1.eval("2+2", "a").verify_result("4")); + TEST_DO(f1.eval("a+2", "b").verify_result("6")); + TEST_DO(f1.eval("a+b").verify_result("10")); +} + +TEST_F("require that symbols can be overwritten", Server()) { + TEST_DO(f1.eval("1", "a").verify_result("1")); + TEST_DO(f1.eval("a+1", "a").verify_result("2")); + TEST_DO(f1.eval("a+1", "a").verify_result("3")); + TEST_DO(f1.eval("a+1", "a").verify_result("4")); +} + +TEST_F("require that tensor result is returned in verbose verbatim form", Server()) { + TEST_DO(f1.eval("1", "a").verify_result("1")); + TEST_DO(f1.eval("2", "b").verify_result("2")); + TEST_DO(f1.eval("3", "c").verify_result("3")); + TEST_DO(f1.eval("tensor(x[3]):[a,b,c]").verify_result("tensor(x[3]):{{x:0}:1,{x:1}:2,{x:2}:3}")); +} + +TEST_F("require that execution steps can be extracted", Server()) { + TEST_DO(f1.eval("1", "a").verify_result("1")); + TEST_DO(f1.eval("2", "b").verify_result("2")); + TEST_DO(f1.eval("3", "c").verify_result("3")); + auto res1 = f1.eval("a+b+c"); + auto res2 = f1.eval("a+b+c", "", true); + EXPECT_EQUAL(res1.steps.size(), 0u); + EXPECT_EQUAL(res2.steps.size(), 5u); + for (const auto &step: res2.steps) { + fprintf(stderr, "step:\n class: %s\n symbol: %s\n", + step.first.c_str(), step.second.c_str()); + } +} + +//----------------------------------------------------------------------------- + +TEST_F("require that operation batching works", Server()) { + Slime req; + auto &arr = req.setArray(); + auto &req1 = arr.addObject(); + req1.setString("expr", "2+2"); + req1.setString("name", "a"); + auto &req2 = arr.addObject(); + req2.setString("expr", "a+2"); + req2.setString("name", "b"); + auto &req3 = arr.addObject(); + req3.setString("expr", "this does not parse"); + auto &req4 = arr.addObject(); + req4.setString("expr", "a+b"); + Slime reply = f1.invoke(req); + EXPECT_EQUAL(reply.get().entries(), 4u); + EXPECT_TRUE(reply[2]["error"].asString().size > 0); + EXPECT_EQUAL(reply[3]["result"].asString().make_string(), "10"); +} + +TEST_F("require that empty operation batch works", Server()) { + Slime req; + req.setArray(); + Slime reply = f1.invoke(req); + EXPECT_TRUE(reply.get().type().getId() == slime::ARRAY::ID); + EXPECT_EQUAL(reply.get().entries(), 0u); +} + +//----------------------------------------------------------------------------- + +TEST_F("require that empty expression produces error", Server()) { + auto res = f1.eval(""); + TEST_DO(res.verify_error("missing expression")); +} + +TEST_F("require that parse error produces error", Server()) { + auto res = f1.eval("this does not parse"); + TEST_DO(res.verify_error("expression parsing failed")); +} + +//----------------------------------------------------------------------------- + +TEST_MAIN_WITH_PROCESS_PROXY() { TEST_RUN_ALL(); } diff --git a/eval/src/vespa/eval/eval/tensor_spec.cpp b/eval/src/vespa/eval/eval/tensor_spec.cpp index 5e833710e8c..a0bed3f551b 100644 --- a/eval/src/vespa/eval/eval/tensor_spec.cpp +++ b/eval/src/vespa/eval/eval/tensor_spec.cpp @@ -287,6 +287,9 @@ TensorSpec::to_slime(slime::Cursor &tensor) const vespalib::string TensorSpec::to_expr() const { + if (_type == "double") { + return make_string("%g", as_double()); + } vespalib::string out = _type; out.append(":{"); CommaTracker cell_list; diff --git a/eval/src/vespa/eval/eval/test/test_io.cpp b/eval/src/vespa/eval/eval/test/test_io.cpp index b53ee864cbe..04cb166d89b 100644 --- a/eval/src/vespa/eval/eval/test/test_io.cpp +++ b/eval/src/vespa/eval/eval/test/test_io.cpp @@ -63,6 +63,39 @@ StdOut::commit(size_t bytes) //----------------------------------------------------------------------------- +bool +LineReader::read_line(vespalib::string &line) +{ + line.clear(); + for (auto mem = _input.obtain(); mem.size > 0; mem = _input.obtain()) { + for (size_t i = 0; i < mem.size; ++i) { + if (mem.data[i] == '\n') { + _input.evict(i + 1); + return true; + } else { + line.push_back(mem.data[i]); + } + } + _input.evict(mem.size); + } + return !line.empty(); +} + +//----------------------------------------------------------------------------- + +bool look_for_eof(Input &input) { + for (auto mem = input.obtain(); mem.size > 0; mem = input.obtain()) { + for (size_t i = 0; i < mem.size; ++i) { + if (!isspace(mem.data[i])) { + input.evict(i); + return false; + } + } + input.evict(mem.size); + } + return true; +} + void write_compact(const Slime &slime, Output &out) { JsonFormat::encode(slime, out, true); out.reserve(1).data[0] = '\n'; diff --git a/eval/src/vespa/eval/eval/test/test_io.h b/eval/src/vespa/eval/eval/test/test_io.h index e57fa7e68a2..81fd00a0781 100644 --- a/eval/src/vespa/eval/eval/test/test_io.h +++ b/eval/src/vespa/eval/eval/test/test_io.h @@ -8,6 +8,7 @@ #include <vespa/vespalib/data/output.h> #include <vespa/vespalib/data/simple_buffer.h> #include <vespa/vespalib/data/slime/slime.h> +#include <vespa/vespalib/util/size_literals.h> #include <functional> namespace vespalib::eval::test { @@ -38,6 +39,22 @@ public: }; /** + * Read one line at a time from an input + **/ +class LineReader { +private: + Input &_input; +public: + LineReader(Input &input) : _input(input) {} + bool read_line(vespalib::string &line); +}; + +/** + * Skip whitespaces from the input and return true if eof was reached. + **/ +bool look_for_eof(Input &input); + +/** * Write a slime structure as compact json with a trailing newline. **/ void write_compact(const Slime &slime, Output &out); |