blob: 6fa6cad8d38befccf5563dc8c7ce9a8588a5981a [file] [log] [blame]
% 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.
-module(couch_js_tests).
-include_lib("eunit/include/eunit.hrl").
couch_js_test_() ->
{
"Test couchjs",
{
setup,
fun test_util:start_couch/0,
fun test_util:stop_couch/1,
[
fun should_create_sandbox/0,
fun should_reset_properly/0,
fun should_freeze_doc_object/0,
fun should_roundtrip_utf8/0,
fun should_roundtrip_modified_utf8/0,
fun should_replace_broken_utf16/0,
fun should_allow_js_string_mutations/0,
{timeout, 60000, fun should_exit_on_oom/0},
{timeout, 60000, fun should_exit_on_internal_error/0}
]
}
}.
%% erlfmt-ignore
should_create_sandbox() ->
% Try and detect whether we can see out of the
% sandbox or not.
Src = <<"
function(doc) {
try {
emit(false, typeof(Couch.compile_function));
} catch (e) {
emit(true, e.message);
}
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, {[]}]),
?assertMatch([[[true, <<_/binary>>]]], Result),
[[[true, ErrMsg]]] = Result,
?assertNotEqual([], binary:matches(ErrMsg, <<"not defined">>)),
?assert(couch_stats:sample([couchdb, query_server, process_starts]) > 0),
?assert(couch_stats:sample([couchdb, query_server, process_prompts]) > 0),
?assert(couch_stats:sample([couchdb, query_server, acquired_processes]) > 0),
?assert(couch_stats:sample([couchdb, query_server, process_exists]) >= 0),
?assert(couch_stats:sample([couchdb, query_server, process_errors]) >= 0),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
should_reset_properly() ->
Src = <<"
function(doc) {
var a = [0,1,2];
emit(a.indexOf(0), Object.foo);
Object.foo = 43;
[].constructor.prototype.indexOf = function(x) {return 42;};
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
Doc = {[]},
Result1 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[0, null]]], Result1),
Result2 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[42, 43]]], Result2),
true = couch_query_servers:proc_prompt(Proc, [<<"reset">>]),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
Result3 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[0, null]]], Result3),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
should_freeze_doc_object() ->
Src = <<"
function(doc) {
emit(doc.foo, doc.bar);
doc.foo = 1042;
doc.bar = 1043;
emit(doc.foo, doc.bar);
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
Doc = {[{<<"bar">>, 1041}]},
Result1 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[null, 1041], [null, 1041]]], Result1),
Result2 = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[null, 1041], [null, 1041]]], Result2),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
should_roundtrip_utf8() ->
% Try round tripping UTF-8 both directions through
% couchjs. These tests use hex encoded values of
% Ä (C384) and Ü (C39C) so as to avoid odd editor/Erlang encoding
% strangeness.
Src = <<
"function(doc) {
emit(doc.value, \"", 16#C3, 16#9C, "\");
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
Doc =
{[
{<<"value">>, <<16#C3, 16#84>>}
]},
Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[<<16#C3, 16#84>>, <<16#C3, 16#9C>>]]], Result),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
should_roundtrip_modified_utf8() ->
% Mimicking the test case from the mailing list
Src = <<"
function(doc) {
emit(doc.value.toLowerCase(), \"", 16#C3, 16#9C, "\");
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
Doc =
{[
{<<"value">>, <<16#C3, 16#84>>}
]},
Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
?assertEqual([[[<<16#C3, 16#A4>>, <<16#C3, 16#9C>>]]], Result),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
should_replace_broken_utf16() ->
% This test reverse the surrogate pair of
% the Boom emoji U+1F4A5
Src = <<"
function(doc) {
emit(doc.value.split(\"\").reverse().join(\"\"), 1);
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
Doc =
{[
{<<"value">>, list_to_binary(xmerl_ucs:to_utf8([16#1F4A5]))}
]},
Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
% Invalid UTF-8 gets replaced with the 16#FFFD replacement
% marker
Markers = list_to_binary(xmerl_ucs:to_utf8([16#FFFD, 16#FFFD])),
?assertEqual([[[Markers, 1]]], Result),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
should_allow_js_string_mutations() ->
% This binary corresponds to this string: мама мыла раму
% Which I'm told translates to: "mom was washing the frame"
MomWashedTheFrame = <<
16#D0,
16#BC,
16#D0,
16#B0,
16#D0,
16#BC,
16#D0,
16#B0,
16#20,
16#D0,
16#BC,
16#D1,
16#8B,
16#D0,
16#BB,
16#D0,
16#B0,
16#20,
16#D1,
16#80,
16#D0,
16#B0,
16#D0,
16#BC,
16#D1,
16#83
>>,
Mom = <<16#D0, 16#BC, 16#D0, 16#B0, 16#D0, 16#BC, 16#D0, 16#B0>>,
Washed = <<16#D0, 16#BC, 16#D1, 16#8B, 16#D0, 16#BB, 16#D0, 16#B0>>,
Src1 = <<"
function(doc) {
emit(\"length\", doc.value.length);
}
">>,
Src2 = <<"
function(doc) {
emit(\"substring\", doc.value.substring(5, 9));
}
">>,
Src3 = <<"
function(doc) {
emit(\"slice\", doc.value.slice(0, 4));
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src1]),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src2]),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src3]),
Doc = {[{<<"value">>, MomWashedTheFrame}]},
Result = couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc]),
Expect = [
[[<<"length">>, 14]],
[[<<"substring">>, Washed]],
[[<<"slice">>, Mom]]
],
?assertEqual(Expect, Result),
couch_query_servers:ret_os_process(Proc).
%% erlfmt-ignore
should_exit_on_oom() ->
Src = <<"
var state = [];
function(doc) {
var val = \"0123456789ABCDEF\";
for(var i = 0; i < 665535; i++) {
state.push([val, val]);
emit(null, null);
}
}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
trigger_oom(Proc).
%% erlfmt-ignore
should_exit_on_internal_error() ->
% A different way to trigger OOM which previously used to
% throw an InternalError on SM. Check that we still exit on that
% type of error
Src = <<"
function(doc) {
function mkstr(l) {
var r = 'r';
while (r.length < l) {r = r + r;}
return r;
}
var s = mkstr(96*1024*1024);
var a = [s,s,s,s,s,s,s,s];
var j = JSON.stringify(a);
emit(42, j.length);}
">>,
Proc = couch_query_servers:get_os_process(<<"javascript">>),
true = couch_query_servers:proc_prompt(Proc, [<<"reset">>]),
true = couch_query_servers:proc_prompt(Proc, [<<"add_fun">>, Src]),
Doc = {[]},
try
couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, Doc])
catch
% Expect either an internal error thrown if it catches it and
% emits an error log before dying
throw:{<<"InternalError">>, _} ->
ok;
% gen_server may die before replying
exit:{noproc, {gen_server,call, _}} ->
ok;
% or it may die with an epipe if it crashes while we send/recv data to it
exit:{epipe, {gen_server, call, _}} ->
ok;
% It may fail and just exit the process. That's expected as well
throw:{os_process_error, _} ->
ok
end,
?assert(couch_stats:sample([couchdb, query_server, process_errors]) > 0).
trigger_oom(Proc) ->
Status =
try
couch_query_servers:proc_prompt(Proc, [<<"map_doc">>, <<"{}">>]),
continue
catch
throw:{os_process_error, {exit_status, 1}} ->
done
end,
case Status of
continue -> trigger_oom(Proc);
done -> ok
end.