diff options
author | Paul Joseph Davis <davisp@apache.org> | 2009-11-09 00:39:16 +0000 |
---|---|---|
committer | Paul Joseph Davis <davisp@apache.org> | 2009-11-09 00:39:16 +0000 |
commit | e29a1924afe9e6051369f7bcbf44ccdf53de536a (patch) | |
tree | e9511858c1ca8faf7b051c38b712982cb051caba /test/view_server | |
parent | c114565bff170785c1352142dc1a28557a91dd97 (diff) |
Fixes 'make distcheck' to run the test suite.
Quite a few changes to the build system to handle VPATH builds appropriately as well as make the test suite know about them.
git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@833951 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'test/view_server')
-rw-r--r-- | test/view_server/Makefile.am | 15 | ||||
-rw-r--r-- | test/view_server/query_server_spec.rb | 695 | ||||
-rwxr-xr-x | test/view_server/run_native_process.es | 55 |
3 files changed, 765 insertions, 0 deletions
diff --git a/test/view_server/Makefile.am b/test/view_server/Makefile.am new file mode 100644 index 00000000..11e7feb4 --- /dev/null +++ b/test/view_server/Makefile.am @@ -0,0 +1,15 @@ +## 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. + +EXTRA_DIST = \ + query_server_spec.rb \ + run_native_process.es diff --git a/test/view_server/query_server_spec.rb b/test/view_server/query_server_spec.rb new file mode 100644 index 00000000..8fc2ab43 --- /dev/null +++ b/test/view_server/query_server_spec.rb @@ -0,0 +1,695 @@ +# 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/query_server_spec.rb -f specdoc --color + +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 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}/src/couchdb/couchjs #{COUCH_ROOT}/share/server/main.js", + "erlang" => "#{COUCH_ROOT}/test/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 + + +functions = { + "emit-twice" => { + "js" => %{function(doc){emit("foo",doc.a); emit("bar",doc.a)}}, + "erlang" => <<-ERLANG + fun({Doc}) -> + A = proplists:get_value(<<"a">>, Doc, null), + Emit(<<"foo">>, A), + Emit(<<"bar">>, A) + end. + ERLANG + }, + "emit-once" => { + "js" => %{function(doc){emit("baz",doc.a)}}, + "erlang" => <<-ERLANG + fun({Doc}) -> + A = proplists: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 proplists: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 = proplists:get_value(<<"title">>, Doc), + Body = proplists: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 = proplists:get_value(<<"title">>, Doc), + Body = proplists: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(proplists:get_value(<<"q">>, Req)), + Fun = fun({Row}, _) -> + Send(proplists: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(proplists: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(proplists:get_value(<<"q">>, Req)), + Fun = fun({Row}, _) -> + Send(proplists: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(proplists:get_value(<<"q">>, Req)), + Fun = fun + ({Row}, Count) when Count < 2 -> + Send(proplists:get_value(<<"key">>, Row)), + {ok, Count+1}; + ({Row}, Count) when Count == 2 -> + Send(proplists: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(proplists:get_value(<<"key">>, Row)), + {ok, Count+1}; + ({Row}, Count) when Count == 2 -> + Send(proplists:get_value(<<"key">>, Row)), + {stop, <<"early">>} + end, + {ok, Tail} = FoldRows(Fun, 0), + Tail + end. + ERLANG + }, + "list-raw" => { + "js" => <<-JS, + function(head, req) { + 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(proplists:get_value(<<"q">>, Req)), + Fun = fun({Row}, _) -> + Send(proplists: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) -> + proplists: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 + } +} + +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 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 + + # it "should validate" + describe "validation" do + before(:all) do + @fun = functions["validate-forbidden"][LANGUAGE] + @qs.reset! + end + it "should allow good updates" do + @qs.run(["validate", @fun, {"good" => true}, {}, {}]).should == 1 + end + it "should reject invalid updates" do + @qs.run(["validate", @fun, {"bad" => true}, {}, {}]).should == {"forbidden"=>"bad doc"} + end + end + + describe "show" do + before(:all) do + @fun = functions["show-simple"][LANGUAGE] + @qs.reset! + end + it "should show" do + @qs.rrun(["show", @fun, + {:title => "Best ever", :body => "Doc body"}, {}]) + @qs.jsgets.should == ["resp", {"body" => "Best ever - Doc body"}] + end + end + + describe "show with headers" do + before(:all) do + @fun = functions["show-headers"][LANGUAGE] + @qs.reset! + end + it "should show headers" do + @qs.rrun(["show", @fun, + {:title => "Best ever", :body => "Doc body"}, {}]) + @qs.jsgets.should == ["resp", {"code"=>200,"headers" => {"X-Plankton"=>"Rusty"}, "body" => "Best ever - Doc body"}] + end + end + +# end +# LIST TESTS +# __END__ + + describe "raw list with headers" do + before(:each) do + @fun = functions["show-sends"][LANGUAGE] + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should do headers proper" do + @qs.rrun(["list", {"total_rows"=>1000}, {"q" => "ok"}]) + @qs.jsgets.should == ["start", ["first chunk", 'second "chunk"'], {"headers"=>{"Content-Type"=>"text/plain"}}] + @qs.rrun(["list_end"]) + @qs.jsgets.should == ["end", ["tail"]] + end + end + + describe "list with rows" do + before(:each) do + @fun = functions["show-while-get-rows"][LANGUAGE] + @qs.run(["reset"]).should == true + @qs.add_fun(@fun).should == true + end + it "should list em" do + @qs.rrun(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + @qs.jsgets.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.rrun(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + @qs.jsgets.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 + before(:all) do + @fun = functions["show-while-get-rows-multi-send"][LANGUAGE] + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should should buffer em" do + @qs.rrun(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + @qs.jsgets.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 + + describe "example list" do + before(:all) do + @fun = functions["list-simple"][LANGUAGE] + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should run normal" do + @qs.run(["list", {"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 "only goes to 2 list" do + before(:all) do + @fun = functions["list-chunky"][LANGUAGE] + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should end early" do + @qs.run(["list", {"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 + + describe "changes filter" do + before(:all) do + @fun = functions["filter-basic"][LANGUAGE] + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should only return true for good docs" do + @qs.run(["filter", [{"key"=>"bam", "good" => true}, {"foo" => "bar"}, {"good" => true}], {"req" => "foo"}]). + should == [true, [true, false, true]] + end + end + + describe "update" do + before(:all) do + @fun = functions["update-basic"][LANGUAGE] + @qs.reset! + end + it "should return a doc and a resp body" do + up, doc, resp = @qs.run(["update", @fun, {"foo" => "gnarly"}, {"verb" => "POST"}]) + up.should == "up" + doc.should == {"foo" => "gnarly", "world" => "hello"} + resp["body"].should == "hello doc" + end + end +end + +def should_have_exited qs + begin + qs.run(["reset"]) + "raise before this".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 + end + after(:each) do + @qs.close + end + + if LANGUAGE == "js" + describe "old style list" do + before(:each) do + @fun = functions["list-old-style"][LANGUAGE] + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should get a warning" do + resp = @qs.run(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + resp["error"].should == "render_error" + resp["reason"].should include("the list API has changed") + end + end + end + + describe "only goes to 2 list" do + before(:each) do + @fun = functions["list-capped"][LANGUAGE] + @qs.reset! + @qs.add_fun(@fun).should == true + end + it "should exit if erlang sends too many rows" do + @qs.run(["list", {"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"]] + @qs.rrun(["list_row", {"key"=>"foox"}]) + @qs.jsgets["error"].should == "query_server_error" + should_have_exited @qs + end + end + + describe "raw list" do + before(:each) do + @fun = functions["list-raw"][LANGUAGE] + @qs.run(["reset"]).should == true + @qs.add_fun(@fun).should == true + end + it "should exit if it gets a non-row in the middle" do + @qs.rrun(["list", {"foo"=>"bar"}, {"q" => "ok"}]) + @qs.jsgets.should == ["start", ["first chunk", "ok"], {"headers"=>{}}] + @qs.run(["reset"])["error"].should == "query_server_error" + should_have_exited @qs + end + end +end diff --git a/test/view_server/run_native_process.es b/test/view_server/run_native_process.es new file mode 100755 index 00000000..dfdc423e --- /dev/null +++ b/test/view_server/run_native_process.es @@ -0,0 +1,55 @@ +#! /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 -> mochijson2: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) -> + case (catch mochijson2:encode(Data)) of + {json_encode, Error} -> write({[{<<"error">>, Error}]}); + Json -> send(Json) + end. + +%log(Mesg) -> +% log(Mesg, []). +%log(Mesg, Params) -> +% io:format(standard_error, Mesg, Params). + +loop(Pid) -> + case read() of + stop -> ok; + Json -> + case (catch couch_native_process:prompt(Pid, Json)) of + {error, Reason} -> + ok = write({[{error, 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). + |