#!/usr/bin/env escript %% -*- erlang -*- % 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. %% XXX: Figure out how to -include("couch_db.hrl") -record(doc, {id= <<"">>, revs={0, []}, body={[]}, attachments=[], deleted=false, meta=[]}). -record(http_db, { url, auth = [], resource = "", headers = [ {"User-Agent", "CouchDB/"++couch_server:get_version()}, {"Accept", "application/json"}, {"Accept-Encoding", "gzip"} ], qs = [], method = get, body = nil, options = [ {response_format,binary}, {inactivity_timeout, 30000} ], retries = 10, pause = 1, conn = nil }). config_files() -> lists:map(fun test_util:build_file/1, [ "etc/couchdb/default_dev.ini", "etc/couchdb/local_dev.ini", "test/etap/random_port.ini" ]). main(_) -> test_util:init_code_path(), etap:plan(12), case (catch test()) of ok -> etap:end_tests(); Other -> etap:diag(io_lib:format("Test died abnormally: ~p", [Other])), etap:bail(Other) end, ok. test() -> couch_server_sup:start_link(config_files()), ibrowse:start(), crypto:start(), couch_server:delete(<<"etap-test-source">>, []), couch_server:delete(<<"etap-test-target">>, []), Dbs1 = setup(), test_all(local, local), ok = teardown(Dbs1), Dbs2 = setup(), test_all(local, remote), ok = teardown(Dbs2), Dbs3 = setup(), test_all(remote, local), ok = teardown(Dbs3), Dbs4 = setup(), test_all(remote, remote), ok = teardown(Dbs4), ok. test_all(SrcType, TgtType) -> test_unchanged_db(SrcType, TgtType), test_multiple_changes(SrcType, TgtType), test_changes_not_missing(SrcType, TgtType). test_unchanged_db(SrcType, TgtType) -> {ok, Pid1} = start_changes_feed(SrcType, 0, false), {ok, Pid2} = start_missing_revs(TgtType, Pid1), etap:is( couch_rep_missing_revs:next(Pid2), complete, io_lib:format( "(~p, ~p) no missing revs if source is unchanged", [SrcType, TgtType]) ). test_multiple_changes(SrcType, TgtType) -> Expect = {2, [generate_change(), generate_change()]}, {ok, Pid1} = start_changes_feed(SrcType, 0, false), {ok, Pid2} = start_missing_revs(TgtType, Pid1), etap:is( get_all_missing_revs(Pid2, {0, []}), Expect, io_lib:format("(~p, ~p) add src docs, get missing tgt revs + high seq", [SrcType, TgtType]) ). test_changes_not_missing(SrcType, TgtType) -> %% put identical changes on source and target Id = couch_uuids:random(), {Id, _Seq, [Rev]} = Expect = generate_change(Id, {[]}, get_db(source)), {Id, _, [Rev]} = generate_change(Id, {[]}, get_db(target)), %% confirm that this change is not in missing revs feed {ok, Pid1} = start_changes_feed(SrcType, 0, false), {ok, Pid2} = start_missing_revs(TgtType, Pid1), {HighSeq, AllRevs} = get_all_missing_revs(Pid2, {0, []}), %% etap:none/3 has a bug, so just define it correctly here etap:is( lists:member(Expect, AllRevs), false, io_lib:format( "(~p, ~p) skip revs that already exist on target", [SrcType, TgtType]) ). generate_change() -> generate_change(couch_uuids:random()). generate_change(Id) -> generate_change(Id, {[]}). generate_change(Id, EJson) -> generate_change(Id, EJson, get_db(source)). generate_change(Id, EJson, Db) -> Doc = couch_doc:from_json_obj(EJson), Seq = get_update_seq(), {ok, Rev} = couch_db:update_doc(Db, Doc#doc{id = Id}, [full_commit]), couch_db:close(Db), {Id, Seq+1, [Rev]}. get_all_missing_revs(Pid, {HighSeq, Revs}) -> case couch_rep_missing_revs:next(Pid) of complete -> {HighSeq, lists:flatten(lists:reverse(Revs))}; {Seq, More} -> get_all_missing_revs(Pid, {Seq, [More|Revs]}) end. get_db(source) -> {ok, Db} = couch_db:open(<<"etap-test-source">>, []), Db; get_db(target) -> {ok, Db} = couch_db:open(<<"etap-test-target">>, []), Db. get_update_seq() -> Db = get_db(source), Seq = couch_db:get_update_seq(Db), couch_db:close(Db), Seq. setup() -> {ok, DbA} = couch_db:create(<<"etap-test-source">>, []), {ok, DbB} = couch_db:create(<<"etap-test-target">>, []), [DbA, DbB]. teardown([DbA, DbB]) -> couch_db:close(DbA), couch_db:close(DbB), couch_server:delete(<<"etap-test-source">>, []), couch_server:delete(<<"etap-test-target">>, []), ok. start_changes_feed(local, Since, Continuous) -> Props = [{<<"continuous">>, Continuous}], couch_rep_changes_feed:start_link(self(), get_db(source), Since, Props); start_changes_feed(remote, Since, Continuous) -> Props = [{<<"continuous">>, Continuous}], Db = #http_db{url = server() ++ "etap-test-source/"}, couch_rep_changes_feed:start_link(self(), Db, Since, Props). server() -> lists:concat([ "http://127.0.0.1:", mochiweb_socket_server:get(couch_httpd, port), "/" ]). couch_rep_pid(Db) -> spawn(fun() -> couch_rep_pid_loop(Db) end). couch_rep_pid_loop(Db) -> receive {'$gen_call', From, get_target_db} -> gen_server:reply(From, {ok, Db}) end, couch_rep_pid_loop(Db). start_missing_revs(local, Changes) -> TargetDb = get_db(target), MainPid = couch_rep_pid(TargetDb), couch_rep_missing_revs:start_link(MainPid, TargetDb, Changes, []); start_missing_revs(remote, Changes) -> TargetDb = #http_db{url = server() ++ "etap-test-target/"}, MainPid = couch_rep_pid(TargetDb), couch_rep_missing_revs:start_link(MainPid, TargetDb, Changes, []).