aboutsummaryrefslogtreecommitdiffstats
path: root/eval
diff options
context:
space:
mode:
authorHåvard Pettersen <havardpe@oath.com>2021-08-25 13:38:20 +0000
committerHåvard Pettersen <havardpe@oath.com>2021-08-30 11:15:57 +0000
commit5b4b8cfc34e7a61971ef43d707446d16ff101d03 (patch)
tree56380802eaf0f4567868979d0ff722edf830d044 /eval
parent2a679a9b0ef339c9b417eacef6b2d31a864c31d9 (diff)
add json-repl (and interactive) modes to vespa-eval-expr
Diffstat (limited to 'eval')
-rw-r--r--eval/CMakeLists.txt1
-rw-r--r--eval/src/apps/eval_expr/eval_expr.cpp300
-rw-r--r--eval/src/tests/apps/eval_expr/CMakeLists.txt9
-rw-r--r--eval/src/tests/apps/eval_expr/eval_expr_test.cpp265
-rw-r--r--eval/src/vespa/eval/eval/tensor_spec.cpp3
-rw-r--r--eval/src/vespa/eval/eval/test/test_io.cpp33
-rw-r--r--eval/src/vespa/eval/eval/test/test_io.h17
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);