# 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 = <>, {[{<<"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