summaryrefslogtreecommitdiff
path: root/test/view_server
diff options
context:
space:
mode:
Diffstat (limited to 'test/view_server')
-rw-r--r--test/view_server/query_server_spec.rb824
-rwxr-xr-xtest/view_server/run_native_process.es59
2 files changed, 883 insertions, 0 deletions
diff --git a/test/view_server/query_server_spec.rb b/test/view_server/query_server_spec.rb
new file mode 100644
index 00000000..de1df5c1
--- /dev/null
+++ b/test/view_server/query_server_spec.rb
@@ -0,0 +1,824 @@
+# Licensed 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.
+
+# to run (requires ruby and rspec):
+# spec test/view_server/query_server_spec.rb -f specdoc --color
+#
+# environment options:
+# QS_TRACE=true
+# shows full output from the query server
+# QS_LANG=lang
+# run tests on the query server (for now, one of: js, erlang)
+#
+
+COUCH_ROOT = "#{File.dirname(__FILE__)}/../.." unless defined?(COUCH_ROOT)
+LANGUAGE = ENV["QS_LANG"] || "js"
+
+puts "Running query server specs for #{LANGUAGE} query server"
+
+require 'spec'
+require 'json'
+
+class OSProcessRunner
+ def self.run
+ trace = ENV["QS_TRACE"] || false
+ puts "launching #{run_command}" if trace
+ if block_given?
+ IO.popen(run_command, "r+") do |io|
+ qs = QueryServerRunner.new(io, trace)
+ yield qs
+ end
+ else
+ io = IO.popen(run_command, "r+")
+ QueryServerRunner.new(io, trace)
+ end
+ end
+ def initialize io, trace = false
+ @qsio = io
+ @trace = trace
+ end
+ def close
+ @qsio.close
+ end
+ def reset!
+ run(["reset"])
+ end
+ def add_fun(fun)
+ run(["add_fun", fun])
+ end
+ def teach_ddoc(ddoc)
+ run(["ddoc", "new", ddoc_id(ddoc), ddoc])
+ end
+ def ddoc_run(ddoc, fun_path, args)
+ run(["ddoc", ddoc_id(ddoc), fun_path, args])
+ end
+ def ddoc_id(ddoc)
+ d_id = ddoc["_id"]
+ raise 'ddoc must have _id' unless d_id
+ d_id
+ end
+ def get_chunks
+ resp = jsgets
+ raise "not a chunk" unless resp.first == "chunks"
+ return resp[1]
+ end
+ def run json
+ rrun json
+ jsgets
+ end
+ def rrun json
+ line = json.to_json
+ puts "run: #{line}" if @trace
+ @qsio.puts line
+ end
+ def rgets
+ resp = @qsio.gets
+ puts "got: #{resp}" if @trace
+ resp
+ end
+ def jsgets
+ resp = rgets
+ # err = @qserr.gets
+ # puts "err: #{err}" if err
+ if resp
+ begin
+ rj = JSON.parse("[#{resp.chomp}]")[0]
+ rescue JSON::ParserError
+ puts "JSON ERROR (dump under trace mode)"
+ # puts resp.chomp
+ while resp = rgets
+ # puts resp.chomp
+ end
+ end
+ if rj.respond_to?(:[]) && rj.is_a?(Array)
+ if rj[0] == "log"
+ log = rj[1]
+ puts "log: #{log}" if @trace
+ rj = jsgets
+ end
+ end
+ rj
+ else
+ raise "no response"
+ end
+ end
+end
+
+class QueryServerRunner < OSProcessRunner
+
+ COMMANDS = {
+ "js" => "#{COUCH_ROOT}/bin/couchjs_dev #{COUCH_ROOT}/share/server/main.js",
+ "erlang" => "#{COUCH_ROOT}/test/view_server/run_native_process.es"
+ }
+
+ def self.run_command
+ COMMANDS[LANGUAGE]
+ end
+end
+
+class ExternalRunner < OSProcessRunner
+ def self.run_command
+ "#{COUCH_ROOT}/src/couchdb/couchjs #{COUCH_ROOT}/share/server/echo.js"
+ end
+end
+
+# we could organize this into a design document per language.
+# that would make testing future languages really easy.
+
+functions = {
+ "emit-twice" => {
+ "js" => %{function(doc){emit("foo",doc.a); emit("bar",doc.a)}},
+ "erlang" => <<-ERLANG
+ fun({Doc}) ->
+ A = couch_util:get_value(<<"a">>, Doc, null),
+ Emit(<<"foo">>, A),
+ Emit(<<"bar">>, A)
+ end.
+ ERLANG
+ },
+ "emit-once" => {
+ "js" => <<-JS,
+ function(doc){
+ emit("baz",doc.a)
+ }
+ JS
+ "erlang" => <<-ERLANG
+ fun({Doc}) ->
+ A = couch_util:get_value(<<"a">>, Doc, null),
+ Emit(<<"baz">>, A)
+ end.
+ ERLANG
+ },
+ "reduce-values-length" => {
+ "js" => %{function(keys, values, rereduce) { return values.length; }},
+ "erlang" => %{fun(Keys, Values, ReReduce) -> length(Values) end.}
+ },
+ "reduce-values-sum" => {
+ "js" => %{function(keys, values, rereduce) { return sum(values); }},
+ "erlang" => %{fun(Keys, Values, ReReduce) -> lists:sum(Values) end.}
+ },
+ "validate-forbidden" => {
+ "js" => <<-JS,
+ function(newDoc, oldDoc, userCtx) {
+ if(newDoc.bad)
+ throw({forbidden:"bad doc"}); "foo bar";
+ }
+ JS
+ "erlang" => <<-ERLANG
+ fun({NewDoc}, _OldDoc, _UserCtx) ->
+ case couch_util:get_value(<<"bad">>, NewDoc) of
+ undefined -> 1;
+ _ -> {[{forbidden, <<"bad doc">>}]}
+ end
+ end.
+ ERLANG
+ },
+ "show-simple" => {
+ "js" => <<-JS,
+ function(doc, req) {
+ log("ok");
+ return [doc.title, doc.body].join(' - ');
+ }
+ JS
+ "erlang" => <<-ERLANG
+ fun({Doc}, Req) ->
+ Title = couch_util:get_value(<<"title">>, Doc),
+ Body = couch_util:get_value(<<"body">>, Doc),
+ Resp = <<Title/binary, " - ", Body/binary>>,
+ {[{<<"body">>, Resp}]}
+ end.
+ ERLANG
+ },
+ "show-headers" => {
+ "js" => <<-JS,
+ function(doc, req) {
+ var resp = {"code":200, "headers":{"X-Plankton":"Rusty"}};
+ resp.body = [doc.title, doc.body].join(' - ');
+ return resp;
+ }
+ JS
+ "erlang" => <<-ERLANG
+ fun({Doc}, Req) ->
+ Title = couch_util:get_value(<<"title">>, Doc),
+ Body = couch_util:get_value(<<"body">>, Doc),
+ Resp = <<Title/binary, " - ", Body/binary>>,
+ {[
+ {<<"code">>, 200},
+ {<<"headers">>, {[{<<"X-Plankton">>, <<"Rusty">>}]}},
+ {<<"body">>, Resp}
+ ]}
+ end.
+ ERLANG
+ },
+ "show-sends" => {
+ "js" => <<-JS,
+ function(head, req) {
+ start({headers:{"Content-Type" : "text/plain"}});
+ send("first chunk");
+ send('second "chunk"');
+ return "tail";
+ };
+ JS
+ "erlang" => <<-ERLANG
+ fun(Head, Req) ->
+ Resp = {[
+ {<<"headers">>, {[{<<"Content-Type">>, <<"text/plain">>}]}}
+ ]},
+ Start(Resp),
+ Send(<<"first chunk">>),
+ Send(<<"second \\\"chunk\\\"">>),
+ <<"tail">>
+ end.
+ ERLANG
+ },
+ "show-while-get-rows" => {
+ "js" => <<-JS,
+ function(head, req) {
+ send("first chunk");
+ send(req.q);
+ var row;
+ log("about to getRow " + typeof(getRow));
+ while(row = getRow()) {
+ send(row.key);
+ };
+ return "tail";
+ };
+ JS
+ "erlang" => <<-ERLANG,
+ fun(Head, {Req}) ->
+ Send(<<"first chunk">>),
+ Send(couch_util:get_value(<<"q">>, Req)),
+ Fun = fun({Row}, _) ->
+ Send(couch_util:get_value(<<"key">>, Row)),
+ {ok, nil}
+ end,
+ {ok, _} = FoldRows(Fun, nil),
+ <<"tail">>
+ end.
+ ERLANG
+ },
+ "show-while-get-rows-multi-send" => {
+ "js" => <<-JS,
+ function(head, req) {
+ send("bacon");
+ var row;
+ log("about to getRow " + typeof(getRow));
+ while(row = getRow()) {
+ send(row.key);
+ send("eggs");
+ };
+ return "tail";
+ };
+ JS
+ "erlang" => <<-ERLANG,
+ fun(Head, Req) ->
+ Send(<<"bacon">>),
+ Fun = fun({Row}, _) ->
+ Send(couch_util:get_value(<<"key">>, Row)),
+ Send(<<"eggs">>),
+ {ok, nil}
+ end,
+ FoldRows(Fun, nil),
+ <<"tail">>
+ end.
+ ERLANG
+ },
+ "list-simple" => {
+ "js" => <<-JS,
+ function(head, req) {
+ send("first chunk");
+ send(req.q);
+ var row;
+ while(row = getRow()) {
+ send(row.key);
+ };
+ return "early";
+ };
+ JS
+ "erlang" => <<-ERLANG,
+ fun(Head, {Req}) ->
+ Send(<<"first chunk">>),
+ Send(couch_util:get_value(<<"q">>, Req)),
+ Fun = fun({Row}, _) ->
+ Send(couch_util:get_value(<<"key">>, Row)),
+ {ok, nil}
+ end,
+ FoldRows(Fun, nil),
+ <<"early">>
+ end.
+ ERLANG
+ },
+ "list-chunky" => {
+ "js" => <<-JS,
+ function(head, req) {
+ send("first chunk");
+ send(req.q);
+ var row, i=0;
+ while(row = getRow()) {
+ send(row.key);
+ i += 1;
+ if (i > 2) {
+ return('early tail');
+ }
+ };
+ };
+ JS
+ "erlang" => <<-ERLANG,
+ fun(Head, {Req}) ->
+ Send(<<"first chunk">>),
+ Send(couch_util:get_value(<<"q">>, Req)),
+ Fun = fun
+ ({Row}, Count) when Count < 2 ->
+ Send(couch_util:get_value(<<"key">>, Row)),
+ {ok, Count+1};
+ ({Row}, Count) when Count == 2 ->
+ Send(couch_util:get_value(<<"key">>, Row)),
+ {stop, <<"early tail">>}
+ end,
+ {ok, Tail} = FoldRows(Fun, 0),
+ Tail
+ end.
+ ERLANG
+ },
+ "list-old-style" => {
+ "js" => <<-JS,
+ function(head, req, foo, bar) {
+ return "stuff";
+ }
+ JS
+ "erlang" => <<-ERLANG,
+ fun(Head, Req, Foo, Bar) ->
+ <<"stuff">>
+ end.
+ ERLANG
+ },
+ "list-capped" => {
+ "js" => <<-JS,
+ function(head, req) {
+ send("bacon")
+ var row, i = 0;
+ while(row = getRow()) {
+ send(row.key);
+ i += 1;
+ if (i > 2) {
+ return('early');
+ }
+ };
+ }
+ JS
+ "erlang" => <<-ERLANG,
+ fun(Head, Req) ->
+ Send(<<"bacon">>),
+ Fun = fun
+ ({Row}, Count) when Count < 2 ->
+ Send(couch_util:get_value(<<"key">>, Row)),
+ {ok, Count+1};
+ ({Row}, Count) when Count == 2 ->
+ Send(couch_util:get_value(<<"key">>, Row)),
+ {stop, <<"early">>}
+ end,
+ {ok, Tail} = FoldRows(Fun, 0),
+ Tail
+ end.
+ ERLANG
+ },
+ "list-raw" => {
+ "js" => <<-JS,
+ function(head, req) {
+ // log(this.toSource());
+ // log(typeof send);
+ send("first chunk");
+ send(req.q);
+ var row;
+ while(row = getRow()) {
+ send(row.key);
+ };
+ return "tail";
+ };
+ JS
+ "erlang" => <<-ERLANG,
+ fun(Head, {Req}) ->
+ Send(<<"first chunk">>),
+ Send(couch_util:get_value(<<"q">>, Req)),
+ Fun = fun({Row}, _) ->
+ Send(couch_util:get_value(<<"key">>, Row)),
+ {ok, nil}
+ end,
+ FoldRows(Fun, nil),
+ <<"tail">>
+ end.
+ ERLANG
+ },
+ "filter-basic" => {
+ "js" => <<-JS,
+ function(doc, req) {
+ if (doc.good) {
+ return true;
+ }
+ }
+ JS
+ "erlang" => <<-ERLANG,
+ fun({Doc}, Req) ->
+ couch_util:get_value(<<"good">>, Doc)
+ end.
+ ERLANG
+ },
+ "update-basic" => {
+ "js" => <<-JS,
+ function(doc, req) {
+ doc.world = "hello";
+ var resp = [doc, "hello doc"];
+ return resp;
+ }
+ JS
+ "erlang" => <<-ERLANG,
+ fun({Doc}, Req) ->
+ Doc2 = [{<<"world">>, <<"hello">>}|Doc],
+ [{Doc2}, {[{<<"body">>, <<"hello doc">>}]}]
+ end.
+ ERLANG
+ },
+ "error" => {
+ "js" => <<-JS,
+ function() {
+ throw(["error","error_key","testing"]);
+ }
+ JS
+ "erlang" => <<-ERLANG
+ fun(A, B) ->
+ throw([<<"error">>,<<"error_key">>,<<"testing">>])
+ end.
+ ERLANG
+ },
+ "fatal" => {
+ "js" => <<-JS,
+ function() {
+ throw(["fatal","error_key","testing"]);
+ }
+ JS
+ "erlang" => <<-ERLANG
+ fun(A, B) ->
+ throw([<<"fatal">>,<<"error_key">>,<<"testing">>])
+ end.
+ ERLANG
+ }
+}
+
+def make_ddoc(fun_path, fun_str)
+ doc = {"_id"=>"foo"}
+ d = doc
+ while p = fun_path.shift
+ l = p
+ if !fun_path.empty?
+ d[p] = {}
+ d = d[p]
+ end
+ end
+ d[l] = fun_str
+ doc
+end
+
+describe "query server normal case" do
+ before(:all) do
+ `cd #{COUCH_ROOT} && make`
+ @qs = QueryServerRunner.run
+ end
+ after(:all) do
+ @qs.close
+ end
+ it "should reset" do
+ @qs.run(["reset"]).should == true
+ end
+ it "should not erase ddocs on reset" do
+ @fun = functions["show-simple"][LANGUAGE]
+ @ddoc = make_ddoc(["shows","simple"], @fun)
+ @qs.teach_ddoc(@ddoc)
+ @qs.run(["reset"]).should == true
+ @qs.ddoc_run(@ddoc,
+ ["shows","simple"],
+ [{:title => "Best ever", :body => "Doc body"}, {}]).should ==
+ ["resp", {"body" => "Best ever - Doc body"}]
+ end
+
+ it "should run map funs" do
+ @qs.reset!
+ @qs.run(["add_fun", functions["emit-twice"][LANGUAGE]]).should == true
+ @qs.run(["add_fun", functions["emit-once"][LANGUAGE]]).should == true
+ rows = @qs.run(["map_doc", {:a => "b"}])
+ rows[0][0].should == ["foo", "b"]
+ rows[0][1].should == ["bar", "b"]
+ rows[1][0].should == ["baz", "b"]
+ end
+ describe "reduce" do
+ before(:all) do
+ @fun = functions["reduce-values-length"][LANGUAGE]
+ @qs.reset!
+ end
+ it "should reduce" do
+ kvs = (0...10).collect{|i|[i,i*2]}
+ @qs.run(["reduce", [@fun], kvs]).should == [true, [10]]
+ end
+ end
+ describe "rereduce" do
+ before(:all) do
+ @fun = functions["reduce-values-sum"][LANGUAGE]
+ @qs.reset!
+ end
+ it "should rereduce" do
+ vs = (0...10).collect{|i|i}
+ @qs.run(["rereduce", [@fun], vs]).should == [true, [45]]
+ end
+ end
+
+ describe "design docs" do
+ before(:all) do
+ @ddoc = {
+ "_id" => "foo"
+ }
+ @qs.reset!
+ end
+ it "should learn design docs" do
+ @qs.teach_ddoc(@ddoc).should == true
+ end
+ end
+
+ # it "should validate"
+ describe "validation" do
+ before(:all) do
+ @fun = functions["validate-forbidden"][LANGUAGE]
+ @ddoc = make_ddoc(["validate_doc_update"], @fun)
+ @qs.teach_ddoc(@ddoc)
+ end
+ it "should allow good updates" do
+ @qs.ddoc_run(@ddoc,
+ ["validate_doc_update"],
+ [{"good" => true}, {}, {}]).should == 1
+ end
+ it "should reject invalid updates" do
+ @qs.ddoc_run(@ddoc,
+ ["validate_doc_update"],
+ [{"bad" => true}, {}, {}]).should == {"forbidden"=>"bad doc"}
+ end
+ end
+
+ describe "show" do
+ before(:all) do
+ @fun = functions["show-simple"][LANGUAGE]
+ @ddoc = make_ddoc(["shows","simple"], @fun)
+ @qs.teach_ddoc(@ddoc)
+ end
+ it "should show" do
+ @qs.ddoc_run(@ddoc,
+ ["shows","simple"],
+ [{:title => "Best ever", :body => "Doc body"}, {}]).should ==
+ ["resp", {"body" => "Best ever - Doc body"}]
+ end
+ end
+
+ describe "show with headers" do
+ before(:all) do
+ # TODO we can make real ddocs up there.
+ @fun = functions["show-headers"][LANGUAGE]
+ @ddoc = make_ddoc(["shows","headers"], @fun)
+ @qs.teach_ddoc(@ddoc)
+ end
+ it "should show headers" do
+ @qs.ddoc_run(
+ @ddoc,
+ ["shows","headers"],
+ [{:title => "Best ever", :body => "Doc body"}, {}]
+ ).
+ should == ["resp", {"code"=>200,"headers" => {"X-Plankton"=>"Rusty"}, "body" => "Best ever - Doc body"}]
+ end
+ end
+
+ describe "recoverable error" do
+ before(:all) do
+ @fun = functions["error"][LANGUAGE]
+ @ddoc = make_ddoc(["shows","error"], @fun)
+ @qs.teach_ddoc(@ddoc)
+ end
+ it "should not exit" do
+ @qs.ddoc_run(@ddoc, ["shows","error"],
+ [{"foo"=>"bar"}, {"q" => "ok"}]).
+ should == ["error", "error_key", "testing"]
+ # still running
+ @qs.run(["reset"]).should == true
+ end
+ end
+
+ describe "changes filter" do
+ before(:all) do
+ @fun = functions["filter-basic"][LANGUAGE]
+ @ddoc = make_ddoc(["filters","basic"], @fun)
+ @qs.teach_ddoc(@ddoc)
+ end
+ it "should only return true for good docs" do
+ @qs.ddoc_run(@ddoc,
+ ["filters","basic"],
+ [[{"key"=>"bam", "good" => true}, {"foo" => "bar"}, {"good" => true}], {"req" => "foo"}]
+ ).
+ should == [true, [true, false, true]]
+ end
+ end
+
+ describe "update" do
+ before(:all) do
+ # in another patch we can remove this duplication
+ # by setting up the design doc for each language ahead of time.
+ @fun = functions["update-basic"][LANGUAGE]
+ @ddoc = make_ddoc(["updates","basic"], @fun)
+ @qs.teach_ddoc(@ddoc)
+ end
+ it "should return a doc and a resp body" do
+ up, doc, resp = @qs.ddoc_run(@ddoc,
+ ["updates","basic"],
+ [{"foo" => "gnarly"}, {"method" => "POST"}]
+ )
+ up.should == "up"
+ doc.should == {"foo" => "gnarly", "world" => "hello"}
+ resp["body"].should == "hello doc"
+ end
+ end
+
+# end
+# LIST TESTS
+# __END__
+
+ describe "ddoc list" do
+ before(:all) do
+ @ddoc = {
+ "_id" => "foo",
+ "lists" => {
+ "simple" => functions["list-simple"][LANGUAGE],
+ "headers" => functions["show-sends"][LANGUAGE],
+ "rows" => functions["show-while-get-rows"][LANGUAGE],
+ "buffer-chunks" => functions["show-while-get-rows-multi-send"][LANGUAGE],
+ "chunky" => functions["list-chunky"][LANGUAGE]
+ }
+ }
+ @qs.teach_ddoc(@ddoc)
+ end
+
+ describe "example list" do
+ it "should run normal" do
+ @qs.ddoc_run(@ddoc,
+ ["lists","simple"],
+ [{"foo"=>"bar"}, {"q" => "ok"}]
+ ).should == ["start", ["first chunk", "ok"], {"headers"=>{}}]
+ @qs.run(["list_row", {"key"=>"baz"}]).should == ["chunks", ["baz"]]
+ @qs.run(["list_row", {"key"=>"bam"}]).should == ["chunks", ["bam"]]
+ @qs.run(["list_row", {"key"=>"foom"}]).should == ["chunks", ["foom"]]
+ @qs.run(["list_row", {"key"=>"fooz"}]).should == ["chunks", ["fooz"]]
+ @qs.run(["list_row", {"key"=>"foox"}]).should == ["chunks", ["foox"]]
+ @qs.run(["list_end"]).should == ["end" , ["early"]]
+ end
+ end
+
+ describe "headers" do
+ it "should do headers proper" do
+ @qs.ddoc_run(@ddoc, ["lists","headers"],
+ [{"total_rows"=>1000}, {"q" => "ok"}]
+ ).should == ["start", ["first chunk", 'second "chunk"'],
+ {"headers"=>{"Content-Type"=>"text/plain"}}]
+ @qs.rrun(["list_end"])
+ @qs.jsgets.should == ["end", ["tail"]]
+ end
+ end
+
+ describe "with rows" do
+ it "should list em" do
+ @qs.ddoc_run(@ddoc, ["lists","rows"],
+ [{"foo"=>"bar"}, {"q" => "ok"}]).
+ should == ["start", ["first chunk", "ok"], {"headers"=>{}}]
+ @qs.rrun(["list_row", {"key"=>"baz"}])
+ @qs.get_chunks.should == ["baz"]
+ @qs.rrun(["list_row", {"key"=>"bam"}])
+ @qs.get_chunks.should == ["bam"]
+ @qs.rrun(["list_end"])
+ @qs.jsgets.should == ["end", ["tail"]]
+ end
+ it "should work with zero rows" do
+ @qs.ddoc_run(@ddoc, ["lists","rows"],
+ [{"foo"=>"bar"}, {"q" => "ok"}]).
+ should == ["start", ["first chunk", "ok"], {"headers"=>{}}]
+ @qs.rrun(["list_end"])
+ @qs.jsgets.should == ["end", ["tail"]]
+ end
+ end
+
+ describe "should buffer multiple chunks sent for a single row." do
+ it "should should buffer em" do
+ @qs.ddoc_run(@ddoc, ["lists","buffer-chunks"],
+ [{"foo"=>"bar"}, {"q" => "ok"}]).
+ should == ["start", ["bacon"], {"headers"=>{}}]
+ @qs.rrun(["list_row", {"key"=>"baz"}])
+ @qs.get_chunks.should == ["baz", "eggs"]
+ @qs.rrun(["list_row", {"key"=>"bam"}])
+ @qs.get_chunks.should == ["bam", "eggs"]
+ @qs.rrun(["list_end"])
+ @qs.jsgets.should == ["end", ["tail"]]
+ end
+ end
+ it "should end after 2" do
+ @qs.ddoc_run(@ddoc, ["lists","chunky"],
+ [{"foo"=>"bar"}, {"q" => "ok"}]).
+ should == ["start", ["first chunk", "ok"], {"headers"=>{}}]
+
+ @qs.run(["list_row", {"key"=>"baz"}]).
+ should == ["chunks", ["baz"]]
+
+ @qs.run(["list_row", {"key"=>"bam"}]).
+ should == ["chunks", ["bam"]]
+
+ @qs.run(["list_row", {"key"=>"foom"}]).
+ should == ["end", ["foom", "early tail"]]
+ # here's where js has to discard quit properly
+ @qs.run(["reset"]).
+ should == true
+ end
+ end
+ end
+
+
+
+def should_have_exited qs
+ begin
+ qs.run(["reset"])
+ "raise before this (except Erlang)".should == true
+ rescue RuntimeError => e
+ e.message.should == "no response"
+ rescue Errno::EPIPE
+ true.should == true
+ end
+end
+
+describe "query server that exits" do
+ before(:each) do
+ @qs = QueryServerRunner.run
+ @ddoc = {
+ "_id" => "foo",
+ "lists" => {
+ "capped" => functions["list-capped"][LANGUAGE],
+ "raw" => functions["list-raw"][LANGUAGE]
+ },
+ "shows" => {
+ "fatal" => functions["fatal"][LANGUAGE]
+ }
+ }
+ @qs.teach_ddoc(@ddoc)
+ end
+ after(:each) do
+ @qs.close
+ end
+
+ describe "only goes to 2 list" do
+ it "should exit if erlang sends too many rows" do
+ @qs.ddoc_run(@ddoc, ["lists","capped"],
+ [{"foo"=>"bar"}, {"q" => "ok"}]).
+ should == ["start", ["bacon"], {"headers"=>{}}]
+ @qs.run(["list_row", {"key"=>"baz"}]).should == ["chunks", ["baz"]]
+ @qs.run(["list_row", {"key"=>"foom"}]).should == ["chunks", ["foom"]]
+ @qs.run(["list_row", {"key"=>"fooz"}]).should == ["end", ["fooz", "early"]]
+ e = @qs.run(["list_row", {"key"=>"foox"}])
+ e[0].should == "error"
+ e[1].should == "unknown_command"
+ should_have_exited @qs
+ end
+ end
+
+ describe "raw list" do
+ it "should exit if it gets a non-row in the middle" do
+ @qs.ddoc_run(@ddoc, ["lists","raw"],
+ [{"foo"=>"bar"}, {"q" => "ok"}]).
+ should == ["start", ["first chunk", "ok"], {"headers"=>{}}]
+ e = @qs.run(["reset"])
+ e[0].should == "error"
+ e[1].should == "list_error"
+ should_have_exited @qs
+ end
+ end
+
+ describe "fatal error" do
+ it "should exit" do
+ @qs.ddoc_run(@ddoc, ["shows","fatal"],
+ [{"foo"=>"bar"}, {"q" => "ok"}]).
+ should == ["error", "error_key", "testing"]
+ should_have_exited @qs
+ end
+ end
+end
+
+describe "thank you for using the tests" do
+ it "for more info run with QS_TRACE=true or see query_server_spec.rb file header" do
+ end
+end \ No newline at end of file
diff --git a/test/view_server/run_native_process.es b/test/view_server/run_native_process.es
new file mode 100755
index 00000000..fcf16d75
--- /dev/null
+++ b/test/view_server/run_native_process.es
@@ -0,0 +1,59 @@
+#! /usr/bin/env escript
+
+% Licensed 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.
+
+read() ->
+ case io:get_line('') of
+ eof -> stop;
+ Data -> couch_util:json_decode(Data)
+ end.
+
+send(Data) when is_binary(Data) ->
+ send(binary_to_list(Data));
+send(Data) when is_list(Data) ->
+ io:format(Data ++ "\n", []).
+
+write(Data) ->
+ % log("~p", [Data]),
+ case (catch couch_util:json_encode(Data)) of
+ % when testing, this is what prints your errors
+ {json_encode, Error} -> write({[{<<"error">>, Error}]});
+ Json -> send(Json)
+ end.
+
+% log(Mesg) ->
+% log(Mesg, []).
+% log(Mesg, Params) ->
+% io:format(standard_error, Mesg, Params).
+% jlog(Mesg) ->
+% write([<<"log">>, list_to_binary(io_lib:format("~p",[Mesg]))]).
+
+loop(Pid) ->
+ case read() of
+ stop -> ok;
+ Json ->
+ case (catch couch_native_process:prompt(Pid, Json)) of
+ {error, Reason} ->
+ ok = write([error, Reason, Reason]);
+ Resp ->
+ ok = write(Resp),
+ loop(Pid)
+ end
+ end.
+
+main([]) ->
+ code:add_pathz("src/couchdb"),
+ code:add_pathz("src/mochiweb"),
+ {ok, Pid} = couch_native_process:start_link(),
+ loop(Pid).
+