From 38ddb2b054db0bef6c2a97f9078621290dc37ca3 Mon Sep 17 00:00:00 2001 From: Juan Facorro Date: Tue, 1 Dec 2015 13:54:36 -0300 Subject: [PATCH] [#98] Added tests. Fixed bugs. Removed unnecessary code. --- .gitignore | 1 + Makefile | 31 ++- erlang.mk | 232 ++++++++++--------- src/shotgun.erl | 89 +++---- test/cover.spec | 8 + test/http_server.app | 14 ++ test/http_server/http_base_handler.erl | 33 +++ test/http_server/http_basic_auth_handler.erl | 33 +++ test/http_server/http_binary_handler.erl | 40 ++++ test/http_server/http_server.erl | 63 +++++ test/http_server/http_server_sup.erl | 14 ++ test/http_server/http_simple_handler.erl | 53 +++++ test/http_server/http_sse_handler.erl | 39 ++++ test/shotgun_SUITE.erl | 199 ++++++++++++++++ test/shotgun_async_SUITE.erl | 180 ++++++++++++++ test/shotgun_meta_SUITE.erl | 8 + test/shotgun_test_utils.erl | 25 ++ 17 files changed, 885 insertions(+), 177 deletions(-) create mode 100644 test/cover.spec create mode 100644 test/http_server.app create mode 100644 test/http_server/http_base_handler.erl create mode 100644 test/http_server/http_basic_auth_handler.erl create mode 100644 test/http_server/http_binary_handler.erl create mode 100644 test/http_server/http_server.erl create mode 100644 test/http_server/http_server_sup.erl create mode 100644 test/http_server/http_simple_handler.erl create mode 100644 test/http_server/http_sse_handler.erl create mode 100644 test/shotgun_SUITE.erl create mode 100644 test/shotgun_async_SUITE.erl create mode 100644 test/shotgun_meta_SUITE.erl create mode 100644 test/shotgun_test_utils.erl diff --git a/.gitignore b/.gitignore index da14abf..6425c89 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ logs storage *.packages.v* elvis +.erlang.mk/ \ No newline at end of file diff --git a/Makefile b/Makefile index 54c5b6b..d5b5b36 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,41 @@ PROJECT = shotgun -DEPS = gun -dep_gun = git https://github.com/ninenines/gun.git 427230d +CONFIG = rel/sys.config +DEPS = cowlib gun +TEST_DEPS = katana cowboy mixer lasse SHELL_DEPS = sync -dep_sync = git git://github.com/inaka/sync.git 0.1.3 + +dep_cowlib = git https://github.com/ninenines/cowlib.git 1.0.2 +dep_gun = git https://github.com/ninenines/gun.git 427230d +dep_katana = git git://github.com/inaka/erlang-katana.git 0.2.14 +dep_cowboy = git git://github.com/ninenines/cowboy.git 1.0.4 +dep_mixer = git git://github.com/inaka/mixer.git 0.1.4 +dep_lasse = git git://github.com/inaka/lasse.git 1.0.1 +dep_sync = git git://github.com/inaka/sync.git 0.1.3 include erlang.mk ERLC_OPTS += +warn_missing_spec -CONFIG = rel/sys.config +CT_OPTS = -cover test/cover.spec -erl_args -config ${CONFIG} SHELL_OPTS = -name ${PROJECT}@`hostname` -s ${PROJECT} -config ${CONFIG} -s sync +quicktests: app + @$(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" + $(verbose) mkdir -p $(CURDIR)/logs/ + $(gen_verbose) $(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) + +test-build-plt: ERLC_OPTS=$(TEST_ERLC_OPTS) +test-build-plt: + @$(MAKE) --no-print-directory test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" + $(gen_verbose) touch ebin/test + +plt-all: PLT_APPS := $(ALL_TEST_DEPS_DIRS) +plt-all: test-deps test-build-plt plt + +dialyze-all: app test-build-plt dialyze + erldocs: all erldocs . -o docs diff --git a/erlang.mk b/erlang.mk index 26cfbfa..74e0752 100644 --- a/erlang.mk +++ b/erlang.mk @@ -16,7 +16,7 @@ ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST))) -ERLANG_MK_VERSION = 2.0.0-pre.1-37-gb7ccb2e +ERLANG_MK_VERSION = 2.0.0-pre.2-13-g5327c56 # Core configuration. @@ -30,9 +30,11 @@ PROJECT_VERSION ?= rolling V ?= 0 verbose_0 = @ +verbose_2 = set -x; verbose = $(verbose_$(V)) gen_verbose_0 = @echo " GEN " $@; +gen_verbose_2 = set -x; gen_verbose = $(gen_verbose_$(V)) # Temporary files directory. @@ -76,13 +78,11 @@ endif # Core targets. -.NOTPARALLEL: - all:: deps app rel # Noop to avoid a Make warning when there's nothing to do. rel:: - $(verbose) echo -n + $(verbose) : check:: clean app tests @@ -192,8 +192,11 @@ ERLANG_MK_BUILD_DIR ?= .erlang.mk.build erlang-mk: git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR) +ifdef ERLANG_MK_COMMIT + cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT) +endif if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi - cd $(ERLANG_MK_BUILD_DIR) && $(if $(ERLANG_MK_COMMIT),git checkout $(ERLANG_MK_COMMIT) &&) $(MAKE) + $(MAKE) -C $(ERLANG_MK_BUILD_DIR) cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk rm -rf $(ERLANG_MK_BUILD_DIR) @@ -429,7 +432,7 @@ pkg_bullet_name = bullet pkg_bullet_description = Simple, reliable, efficient streaming for Cowboy. pkg_bullet_homepage = http://ninenines.eu pkg_bullet_fetch = git -pkg_bullet_repo = https://github.com/extend/bullet +pkg_bullet_repo = https://github.com/ninenines/bullet pkg_bullet_commit = master PACKAGES += cache @@ -2032,14 +2035,6 @@ pkg_iso8601_fetch = git pkg_iso8601_repo = https://github.com/seansawyer/erlang_iso8601 pkg_iso8601_commit = master -PACKAGES += itweet -pkg_itweet_name = itweet -pkg_itweet_description = Twitter Stream API on ibrowse -pkg_itweet_homepage = http://inaka.github.com/itweet/ -pkg_itweet_fetch = git -pkg_itweet_repo = https://github.com/inaka/itweet -pkg_itweet_commit = v2.0 - PACKAGES += jamdb_sybase pkg_jamdb_sybase_name = jamdb_sybase pkg_jamdb_sybase_description = Erlang driver for SAP Sybase ASE @@ -2696,6 +2691,14 @@ pkg_nkpacket_fetch = git pkg_nkpacket_repo = https://github.com/Nekso/nkpacket pkg_nkpacket_commit = master +PACKAGES += nksip +pkg_nksip_name = nksip +pkg_nksip_description = Erlang SIP application server +pkg_nksip_homepage = https://github.com/kalta/nksip +pkg_nksip_fetch = git +pkg_nksip_repo = https://github.com/kalta/nksip +pkg_nksip_commit = master + PACKAGES += nodefinder pkg_nodefinder_name = nodefinder pkg_nodefinder_description = automatic node discovery via UDP multicast @@ -2960,14 +2963,6 @@ pkg_psycho_fetch = git pkg_psycho_repo = https://github.com/gar1t/psycho pkg_psycho_commit = master -PACKAGES += ptrackerl -pkg_ptrackerl_name = ptrackerl -pkg_ptrackerl_description = Pivotal Tracker API Client written in Erlang -pkg_ptrackerl_homepage = https://github.com/inaka/ptrackerl -pkg_ptrackerl_fetch = git -pkg_ptrackerl_repo = https://github.com/inaka/ptrackerl -pkg_ptrackerl_commit = master - PACKAGES += purity pkg_purity_name = purity pkg_purity_description = A side-effect analyzer for Erlang @@ -4056,6 +4051,7 @@ export NO_AUTOPATCH # Verbosity. dep_verbose_0 = @echo " DEP " $(1); +dep_verbose_2 = set -x; dep_verbose = $(dep_verbose_$(V)) # Core targets. @@ -4075,7 +4071,7 @@ endif $(verbose) mkdir -p $(ERLANG_MK_TMP) $(verbose) for dep in $(ALL_DEPS_DIRS) ; do \ if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \ - echo -n; \ + :; \ else \ echo $$dep >> $(ERLANG_MK_TMP)/deps.log; \ if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \ @@ -4140,7 +4136,7 @@ define dep_autopatch_erlang_mk endef else define dep_autopatch_erlang_mk - echo -n + : endef endif @@ -4192,7 +4188,7 @@ define dep_autopatch_rebar.erl file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/Makefile)", Text, [append]) end, Escape = fun (Text) -> - re:replace(Text, "\\\\$$$$", "\$$$$$$$$", [global, {return, list}]) + re:replace(Text, "\\\\$$", "\$$$$", [global, {return, list}]) end, Write("IGNORE_DEPS += edown eper eunit_formatters meck node_package " "rebar_lock_deps_plugin rebar_vsn_plugin reltool_util\n"), @@ -4315,10 +4311,10 @@ define dep_autopatch_rebar.erl Write("\npre-app::\n"), PatchHook = fun(Cmd) -> case Cmd of - "make -C" ++ Cmd1 -> "$$$$\(MAKE) -C" ++ Escape(Cmd1); - "gmake -C" ++ Cmd1 -> "$$$$\(MAKE) -C" ++ Escape(Cmd1); - "make " ++ Cmd1 -> "$$$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); - "gmake " ++ Cmd1 -> "$$$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); + "make -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); + "gmake -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); + "make " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); + "gmake " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); _ -> Escape(Cmd) end end, @@ -4330,10 +4326,10 @@ define dep_autopatch_rebar.erl {'get-deps', Cmd} -> Write("\npre-deps::\n\t" ++ PatchHook(Cmd) ++ "\n"); {compile, Cmd} -> - Write("\npre-app::\n\tCC=$$$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); + Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); {Regex, compile, Cmd} -> case rebar_utils:is_arch(Regex) of - true -> Write("\npre-app::\n\tCC=$$$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); + true -> Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); false -> ok end; _ -> ok @@ -4341,7 +4337,7 @@ define dep_autopatch_rebar.erl end end(), ShellToMk = fun(V) -> - re:replace(re:replace(V, "(\\\\$$$$)(\\\\w*)", "\\\\1(\\\\2)", [global]), + re:replace(re:replace(V, "(\\\\$$)(\\\\w*)", "\\\\1(\\\\2)", [global]), "-Werror\\\\b", "", [{return, list}, global]) end, PortSpecs = fun() -> @@ -4375,7 +4371,7 @@ define dep_autopatch_rebar.erl case PortSpecs of [] -> ok; _ -> - Write("\npre-app::\n\t$$$$\(MAKE) -f c_src/Makefile.erlang.mk\n"), + Write("\npre-app::\n\t$$\(MAKE) -f c_src/Makefile.erlang.mk\n"), PortSpecWrite(io_lib:format("ERL_CFLAGS = -finline-functions -Wall -fPIC -I ~s/erts-~s/include -I ~s\n", [code:root_dir(), erlang:system_info(version), code:lib_dir(erl_interface, include)])), PortSpecWrite(io_lib:format("ERL_LDFLAGS = -L ~s -lerl_interface -lei\n", @@ -4413,14 +4409,14 @@ define dep_autopatch_rebar.erl _ -> "" end, "\n\nall:: ", Output, "\n\n", - "%.o: %.c\n\t$$$$\(CC) -c -o $$$$\@ $$$$\< $$$$\(CFLAGS) $$$$\(ERL_CFLAGS) $$$$\(DRV_CFLAGS) $$$$\(EXE_CFLAGS)\n\n", - "%.o: %.C\n\t$$$$\(CXX) -c -o $$$$\@ $$$$\< $$$$\(CXXFLAGS) $$$$\(ERL_CFLAGS) $$$$\(DRV_CFLAGS) $$$$\(EXE_CFLAGS)\n\n", - "%.o: %.cc\n\t$$$$\(CXX) -c -o $$$$\@ $$$$\< $$$$\(CXXFLAGS) $$$$\(ERL_CFLAGS) $$$$\(DRV_CFLAGS) $$$$\(EXE_CFLAGS)\n\n", - "%.o: %.cpp\n\t$$$$\(CXX) -c -o $$$$\@ $$$$\< $$$$\(CXXFLAGS) $$$$\(ERL_CFLAGS) $$$$\(DRV_CFLAGS) $$$$\(EXE_CFLAGS)\n\n", + "%.o: %.c\n\t$$\(CC) -c -o $$\@ $$\< $$\(CFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", + "%.o: %.C\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", + "%.o: %.cc\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", + "%.o: %.cpp\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", [[Output, ": ", K, " = ", ShellToMk(V), "\n"] || {K, V} <- lists:reverse(MergeEnv(FilterEnv(Env)))], - Output, ": $$$$\(foreach ext,.c .C .cc .cpp,", - "$$$$\(patsubst %$$$$\(ext),%.o,$$$$\(filter %$$$$\(ext),$$$$\(wildcard", Input, "))))\n", - "\t$$$$\(CC) -o $$$$\@ $$$$\? $$$$\(LDFLAGS) $$$$\(ERL_LDFLAGS) $$$$\(DRV_LDFLAGS) $$$$\(EXE_LDFLAGS)", + Output, ": $$\(foreach ext,.c .C .cc .cpp,", + "$$\(patsubst %$$\(ext),%.o,$$\(filter %$$\(ext),$$\(wildcard", Input, "))))\n", + "\t$$\(CC) -o $$\@ $$\? $$\(LDFLAGS) $$\(ERL_LDFLAGS) $$\(DRV_LDFLAGS) $$\(EXE_LDFLAGS)", case filename:extension(Output) of [] -> "\n"; _ -> " -shared\n" @@ -4482,7 +4478,7 @@ define dep_autopatch_app.erl false -> ok; true -> {ok, [{application, '$(1)', L0}]} = file:consult(App), - Mods = filelib:fold_files("$(call core_native_path,$(DEPS_DIR)/$1/src)", "\\\\.erl$$$$", true, + Mods = filelib:fold_files("$(call core_native_path,$(DEPS_DIR)/$1/src)", "\\\\.erl$$", true, fun (F, Acc) -> [list_to_atom(filename:rootname(filename:basename(F)))|Acc] end, []), L = lists:keystore(modules, 1, L0, {modules, Mods}), ok = file:write_file(App, io_lib:format("~p.~n", [{application, '$(1)', L}])) @@ -4604,7 +4600,7 @@ ifeq ($(filter $(1),$(NO_AUTOPATCH)),) git clone https://github.com/rabbitmq/rabbitmq-codegen.git $(DEPS_DIR)/rabbitmq-codegen; \ fi \ else \ - $(call dep_autopatch,$(DEP_NAME)) \ + $$(call dep_autopatch,$(DEP_NAME)) \ fi endif endef @@ -4649,6 +4645,59 @@ $(foreach p,$(DEP_PLUGINS),\ $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ $(call core_dep_plugin,$p/plugins.mk,$p)))) +# Copyright (c) 2013-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +# Configuration. + +DTL_FULL_PATH ?= +DTL_PATH ?= templates/ +DTL_SUFFIX ?= _dtl + +# Verbosity. + +dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); +dtl_verbose = $(dtl_verbose_$(V)) + +# Core targets. + +define erlydtl_compile.erl + [begin + Module0 = case "$(strip $(DTL_FULL_PATH))" of + "" -> + filename:basename(F, ".dtl"); + _ -> + "$(DTL_PATH)" ++ F2 = filename:rootname(F, ".dtl"), + re:replace(F2, "/", "_", [{return, list}, global]) + end, + Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), + case erlydtl:compile(F, Module, [{out_dir, "ebin/"}, return_errors, {doc_root, "templates"}]) of + ok -> ok; + {ok, _} -> ok + end + end || F <- string:tokens("$(1)", " ")], + halt(). +endef + +ifneq ($(wildcard src/),) + +DTL_FILES = $(sort $(call core_find,$(DTL_PATH),*.dtl)) + +ifdef DTL_FULL_PATH +BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(subst /,_,$(DTL_FILES:$(DTL_PATH)%=%)))) +else +BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(notdir $(DTL_FILES)))) +endif + +# Rebuild templates when the Makefile changes. +$(DTL_FILES): $(MAKEFILE_LIST) + @touch $@ + +ebin/$(PROJECT).app:: $(DTL_FILES) + $(if $(strip $?),\ + $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$?,-pa ebin/ $(DEPS_DIR)/erlydtl/ebin/))) +endif + # Copyright (c) 2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. @@ -4702,25 +4751,32 @@ COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST # Verbosity. app_verbose_0 = @echo " APP " $(PROJECT); +app_verbose_2 = set -x; app_verbose = $(app_verbose_$(V)) appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; +appsrc_verbose_2 = set -x; appsrc_verbose = $(appsrc_verbose_$(V)) makedep_verbose_0 = @echo " DEPEND" $(PROJECT).d; +makedep_verbose_2 = set -x; makedep_verbose = $(makedep_verbose_$(V)) erlc_verbose_0 = @echo " ERLC " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\ $(filter %.erl %.core,$(?F))); +erlc_verbose_2 = set -x; erlc_verbose = $(erlc_verbose_$(V)) xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F)); +xyrl_verbose_2 = set -x; xyrl_verbose = $(xyrl_verbose_$(V)) asn1_verbose_0 = @echo " ASN1 " $(filter %.asn1,$(?F)); +asn1_verbose_2 = set -x; asn1_verbose = $(asn1_verbose_$(V)) mib_verbose_0 = @echo " MIB " $(filter %.bin %.mib,$(?F)); +mib_verbose_2 = set -x; mib_verbose = $(mib_verbose_$(V)) ifneq ($(wildcard src/),) @@ -4728,10 +4784,10 @@ ifneq ($(wildcard src/),) # Targets. ifeq ($(wildcard ebin/test),) -app:: $(PROJECT).d +app:: deps $(PROJECT).d $(verbose) $(MAKE) --no-print-directory app-build else -app:: clean $(PROJECT).d +app:: clean deps $(PROJECT).d $(verbose) $(MAKE) --no-print-directory app-build endif @@ -4761,7 +4817,7 @@ endef endif app-build: ebin/$(PROJECT).app - $(verbose) echo -n + $(verbose) : # Source files. @@ -5424,7 +5480,7 @@ endef # Plugin-specific targets. define render_template - $(verbose) echo "$${_$(1)}" > $(2) + $(verbose) printf -- '$(subst $(newline),\n,$(subst %,%%,$(subst ','\'',$(subst $(tab),$(WS),$(call $(1))))))\n' > $(2) endef ifndef WS @@ -5435,10 +5491,6 @@ WS = $(tab) endif endif -$(foreach template,$(filter bs_% tpl_%,$(.VARIABLES)), \ - $(eval _$(template) = $$(subst $$(tab),$$(WS),$$($(template)))) \ - $(eval export _$(template))) - bootstrap: ifneq ($(wildcard src/),) $(error Error: src/ directory already exists) @@ -5724,10 +5776,6 @@ hello(_) -> erlang:nif_error({not_loaded, ?MODULE}). endef -$(foreach template,bs_c_nif bs_erl_nif, \ - $(eval _$(template) = $$(subst $$(tab),$$(WS),$$($(template)))) \ - $(eval export _$(template))) - new-nif: ifneq ($(wildcard $(C_SRC_DIR)/$n.c),) $(error Error: $(C_SRC_DIR)/$n.c already exists) @@ -5876,9 +5924,8 @@ DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt export DIALYZER_PLT PLT_APPS ?= -DIALYZER_DIRS ?= --src -r src -DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions \ - -Wunmatched_returns # -Wunderspecs +DIALYZER_DIRS ?= --src -r $(wildcard src) $(ALL_APPS_DIRS) +DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions -Wunmatched_returns # -Wunderspecs # Core targets. @@ -5894,6 +5941,18 @@ help:: # Plugin-specific targets. +define filter_opts.erl + Opts = binary:split(<<"$1">>, <<"-">>, [global]), + Filtered = lists:reverse(lists:foldl(fun + (O = <<"pa ", _/bits>>, Acc) -> [O|Acc]; + (O = <<"D ", _/bits>>, Acc) -> [O|Acc]; + (O = <<"I ", _/bits>>, Acc) -> [O|Acc]; + (_, Acc) -> Acc + end, [], Opts)), + io:format("~s~n", [[["-", O] || O <- Filtered]]), + halt(). +endef + $(DIALYZER_PLT): deps app $(verbose) dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS) @@ -5907,7 +5966,7 @@ dialyze: else dialyze: $(DIALYZER_PLT) endif - $(verbose) dialyzer --no_native $(DIALYZER_DIRS) $(DIALYZER_OPTS) + $(verbose) dialyzer --no_native `$(call erlang,$(call filter_opts.erl,$(ERLC_OPTS)))` $(DIALYZER_DIRS) $(DIALYZER_OPTS) # Copyright (c) 2013-2015, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. @@ -5972,59 +6031,6 @@ elvis: $(ELVIS) $(ELVIS_CONFIG) distclean-elvis: $(gen_verbose) rm -rf $(ELVIS) -# Copyright (c) 2013-2015, Loïc Hoguin -# This file is part of erlang.mk and subject to the terms of the ISC License. - -# Configuration. - -DTL_FULL_PATH ?= -DTL_PATH ?= templates/ -DTL_SUFFIX ?= _dtl - -# Verbosity. - -dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); -dtl_verbose = $(dtl_verbose_$(V)) - -# Core targets. - -define erlydtl_compile.erl - [begin - Module0 = case "$(strip $(DTL_FULL_PATH))" of - "" -> - filename:basename(F, ".dtl"); - _ -> - "$(DTL_PATH)" ++ F2 = filename:rootname(F, ".dtl"), - re:replace(F2, "/", "_", [{return, list}, global]) - end, - Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), - case erlydtl:compile(F, Module, [{out_dir, "ebin/"}, return_errors, {doc_root, "templates"}]) of - ok -> ok; - {ok, _} -> ok - end - end || F <- string:tokens("$(1)", " ")], - halt(). -endef - -ifneq ($(wildcard src/),) - -DTL_FILES = $(sort $(call core_find,$(DTL_PATH),*.dtl)) - -ifdef DTL_FULL_PATH -BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(subst /,_,$(DTL_FILES:$(DTL_PATH)%=%)))) -else -BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(notdir $(DTL_FILES)))) -endif - -# Rebuild templates when the Makefile changes. -$(DTL_FILES): $(MAKEFILE_LIST) - @touch $@ - -ebin/$(PROJECT).app:: $(DTL_FILES) - $(if $(strip $?),\ - $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$?,-pa ebin/ $(DEPS_DIR)/erlydtl/ebin/))) -endif - # Copyright (c) 2014 Dave Cottlehuber # This file is part of erlang.mk and subject to the terms of the ISC License. @@ -6165,7 +6171,7 @@ endif ifeq ($(IS_DEP),) ifneq ($(wildcard $(RELX_CONFIG)),) -rel:: distclean-relx-rel relx-rel +rel:: relx-rel endif endif @@ -6177,7 +6183,7 @@ $(RELX): $(gen_verbose) $(call core_http_get,$(RELX),$(RELX_URL)) $(verbose) chmod +x $(RELX) -relx-rel: $(RELX) rel-deps +relx-rel: $(RELX) rel-deps app $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) distclean-relx-rel: diff --git a/src/shotgun.erl b/src/shotgun.erl index 184d1ad..0fa9df3 100644 --- a/src/shotgun.erl +++ b/src/shotgun.erl @@ -90,7 +90,6 @@ #{ async => boolean() , async_mode => binary | sse , handle_event => fun((fin | nofin, reference(), binary()) -> any()) - , basic_auth => {string(), string()} , timeout => timeout() %% Default 5000 ms }. @@ -133,14 +132,14 @@ start_link(Host, Port, Type, Opts) -> %% @equiv get(Host, Port, http, #{}) -spec open(Host :: string(), Port :: integer()) -> - {ok, pid()} | {error, gun_open_failed} | {error, gun_timeout}. + {ok, pid()} | {error, gun_open_failed | gun_open_timeout}. open(Host, Port) -> open(Host, Port, http). -spec open(Host :: string(), Port :: integer(), Type :: connection_type()) -> - {ok, pid()} | {error, gun_open_failed} | {error, gun_timeout}; + {ok, pid()} | {error, gun_open_failed | gun_open_timeout}; (Host :: string(), Port :: integer(), Opts :: open_opts()) -> - {ok, pid()} | {error, gun_open_failed} | {error, gun_timeout}. + {ok, pid()} | {error, gun_open_failed | gun_open_timeout}. %% @equiv get(Host, Port, Type, #{}) or get(Host, Port, http, Opts) open(Host, Port, Type) when is_atom(Type) -> open(Host, Port, Type, #{}); @@ -153,7 +152,7 @@ open(Host, Port, Opts) when is_map(Opts) -> %% transport options. -spec open(Host :: string(), Port :: integer(), Type :: connection_type(), Opts :: open_opts()) -> - {ok, pid()} | {error, gun_open_failed} | {error, gun_timeout}. + {ok, pid()} | {error, gun_open_failed, gun_open_timeout}. open(Host, Port, Type, Opts) -> supervisor:start_child(shotgun_sup, [Host, Port, Type, Opts]). @@ -418,9 +417,6 @@ terminate(_Reason, _StateName, #{pid := Pid} = _State) -> %% @private -spec at_rest(term(), pid(), term()) -> term(). -at_rest({data, _, _} = Event, _From, State) -> - unexpected_event_warning(at_rest, Event), - {reply, {error, unexpected}, at_rest, State}; at_rest(Event, From, State) -> enqueue_work_or_stop(at_rest, Event, From, State). @@ -436,17 +432,11 @@ wait_response(Event, From, State) -> %% @private -spec receive_data(term(), pid(), term()) -> term(). -receive_data({data, _, _} = Event, _From, State) -> - unexpected_event_warning(at_rest, Event), - {reply, {error, unexpected}, receive_data, State}; receive_data(Event, From, State) -> enqueue_work_or_stop(receive_data, Event, From, State). %% @private -spec receive_chunk(term(), pid(), term()) -> term(). -receive_chunk({data, _, _} = Event, _From, State) -> - unexpected_event_warning(at_rest, Event), - {reply, {error, unexpected}, receive_chunk, State}; receive_chunk(Event, From, State) -> enqueue_work_or_stop(receive_chunk, Event, From, State). @@ -494,25 +484,15 @@ at_rest({HttpVerb, {_, _, Body} = Args, From}, State = #{pid := Pid}) -> wait_response({'DOWN', _, _, _, Reason}, _State) -> exit(Reason); wait_response({gun_response, _Pid, _StreamRef, fin, StatusCode, Headers}, - #{from := From, - async := Async, - responses := Responses} = State) -> + #{from := From} = State) -> Response = #{status_code => StatusCode, headers => Headers}, - NewResponses = - case Async of - false -> - gen_fsm:reply(From, {ok, Response}), - Responses; - true -> - gen_fsm:reply(From, {ok, Response}), - queue:in(Response, Responses) - end, - {next_state, at_rest, State#{responses => NewResponses}, 0}; + gen_fsm:reply(From, {ok, Response}), + {next_state, at_rest, State, 0}; wait_response({gun_response, _Pid, _StreamRef, nofin, StatusCode, Headers}, #{from := From, stream := StreamRef, async := Async} = State) -> StateName = case lists:keyfind(<<"transfer-encoding">>, 1, Headers) of - {<<"transfer-encoding">>, <<"chunked">>} when Async == true-> + {<<"transfer-encoding">>, <<"chunked">>} when Async -> Result = {ok, StreamRef}, gen_fsm:reply(From, Result), receive_chunk; @@ -579,26 +559,27 @@ receive_chunk({gun_error, _Pid, _StreamRef, _Reason}, State) -> %% @private -spec clean_state() -> map(). -clean_state() -> clean_state(queue:new()). +clean_state() -> + clean_state(#{}). %% @private -spec clean_state(map()) -> map(); (queue:queue()) -> map(). -clean_state(State) when is_map(State) -> - clean_state(get_pending_reqs(State)); -clean_state(Reqs) -> +clean_state(State) -> + Responses = maps:get(responses, State, queue:new()), + Requests = maps:get(pending_requests, State, queue:new()), #{ pid => undefined, stream => undefined, handle_event => undefined, from => undefined, - responses => queue:new(), + responses => Responses, data => <<"">>, status_code => undefined, headers => undefined, async => false, async_mode => binary, buffer => <<"">>, - pending_requests => Reqs + pending_requests => Requests }. %% @private @@ -712,24 +693,25 @@ check_uri(U) -> %% @private -spec enqueue_work_or_stop(atom(), term(), pid(), state()) -> - {stop, {unexpected, term()}, state()} | - {next_state, atom(), state(), timeout}. -enqueue_work_or_stop(FSM = at_rest, Event, From, State) -> - enqueue_work_or_stop(FSM, Event, From, State, 0); -enqueue_work_or_stop(FSM, Event, From, State) -> - enqueue_work_or_stop(FSM, Event, From, State, infinity). + {stop, {error, any()}, atom(), state()} | + {next_state, atom(), state(), timeout()}. +enqueue_work_or_stop(StateName = at_rest, Event, From, State) -> + enqueue_work_or_stop(StateName, Event, From, State, 0); +enqueue_work_or_stop(StateName, Event, From, State) -> + enqueue_work_or_stop(StateName, Event, From, State, infinity). %% @private -spec enqueue_work_or_stop(atom(), term(), pid(), state(), timeout()) -> - {stop, {unexpected, term()}, state()} | - {next_state, atom(), state(), timeout}. -enqueue_work_or_stop(FSM, Event, From, State, Timeout) -> + {reply, {error, any()}, atom(), state()} | + {next_state, atom(), state(), timeout()}. +enqueue_work_or_stop(StateName, Event, From, State, Timeout) -> case create_work(Event, From) of {ok, Work} -> NewState = append_work(Work, State), - {next_state, FSM, NewState, Timeout}; + {next_state, StateName, NewState, Timeout}; not_work -> - {stop, {unexpected, Event}, State} + Error = {error, {unexpected, Event}}, + {reply, Error, StateName, State} end. %% @private @@ -762,18 +744,5 @@ get_work(State) -> %% @private -spec append_work(work(), state()) -> state(). append_work(Work, State) -> - PendingReqs = get_pending_reqs(State), - NewPending = queue:in(Work, PendingReqs), - maps:put(pending_requests, NewPending, State). - -%% @private --spec get_pending_reqs(state()) -> queue:queue(). -get_pending_reqs(State) -> - maps:get(pending_requests, State). - -%% @private --spec unexpected_event_warning(atom(), any()) -> ok. -unexpected_event_warning(StateName, Event) -> - error_logger:warning_msg( "Unexpected event in state '~p': ~p~n" - , [StateName, Event] - ). + #{pending_requests := PendingReqs} = State, + State#{pending_requests := queue:in(Work, PendingReqs)}. diff --git a/test/cover.spec b/test/cover.spec new file mode 100644 index 0000000..dec8b48 --- /dev/null +++ b/test/cover.spec @@ -0,0 +1,8 @@ +%% Specific modules to include in cover. +{ + incl_mods, + [ shotgun + , shotgun_app + , shotgun_sup + ] +}. diff --git a/test/http_server.app b/test/http_server.app new file mode 100644 index 0000000..2544f8e --- /dev/null +++ b/test/http_server.app @@ -0,0 +1,14 @@ +{application, http_server, + [ + {description, "Cowboy Basic Server."}, + {vsn, "0.1"}, + {applications, + [kernel, + stdlib, + cowboy + ]}, + {modules, []}, + {mod, {http_server, []}}, + {start_phases, [{start_cowboy_http, []}]} + ] +}. diff --git a/test/http_server/http_base_handler.erl b/test/http_server/http_base_handler.erl new file mode 100644 index 0000000..9aefaf7 --- /dev/null +++ b/test/http_server/http_base_handler.erl @@ -0,0 +1,33 @@ +-module(http_base_handler). + +-export([ init/3 + , rest_init/2 + , content_types_accepted/2 + , content_types_provided/2 + , forbidden/2 + , resource_exists/2 + ]). + +%% cowboy +init(_Transport, _Req, _Opts) -> + {upgrade, protocol, cowboy_rest}. + +rest_init(Req, _Opts) -> + {ok, Req, #{}}. + +content_types_accepted(Req, State) -> + {[{<<"text/plain">>, handle_post}], Req, State}. + +content_types_provided(Req, State) -> + {Method, Req1} = cowboy_req:method(Req), + Handler = case Method of + <<"HEAD">> -> handle_head; + _ -> handle_get + end, + {[{<<"text/plain">>, Handler}], Req1, State}. + +forbidden(Req, State) -> + {false, Req, State}. + +resource_exists(Req, State) -> + {true, Req, State}. diff --git a/test/http_server/http_basic_auth_handler.erl b/test/http_server/http_basic_auth_handler.erl new file mode 100644 index 0000000..6d68b6b --- /dev/null +++ b/test/http_server/http_basic_auth_handler.erl @@ -0,0 +1,33 @@ +-module(http_basic_auth_handler). + +-include_lib("mixer/include/mixer.hrl"). +-mixin([{ http_base_handler, + [ init/3 + , rest_init/2 + , content_types_accepted/2 + , content_types_provided/2 + , resource_exists/2 + ]} + ]). + +-export([ allowed_methods/2 + , is_authorized/2 + , handle_get/2 + ]). + +%% cowboy +allowed_methods(Req, State) -> + {[<<"GET">>], Req, State}. + +is_authorized(Req, State) -> + case cowboy_req:parse_header(<<"authorization">>, Req) of + {ok, {<<"basic">>, {<<"user">>, <<"pass">>}}, Req2} -> + {true, Req2, State}; + {_, _, Req2} -> + {{false, <<>>}, Req2, State} + end. + +%% internal +handle_get(Req, State) -> + Body = [<<"Secret information">>], + {Body, Req, State}. diff --git a/test/http_server/http_binary_handler.erl b/test/http_server/http_binary_handler.erl new file mode 100644 index 0000000..0c105f6 --- /dev/null +++ b/test/http_server/http_binary_handler.erl @@ -0,0 +1,40 @@ +-module(http_binary_handler). + +-export([ init/3 + , info/3 + , terminate/3 + ]). + +-spec init(any(), cowboy_req:req(), any()) -> + {loop | shutdown, any(), integer()}. +init(_Transport, Req, _Opts) -> + case cowboy_req:method(Req) of + {<<"GET">>, Req1} -> + Headers = [{<<"content-type">>, <<"text/event-stream">>}, + {<<"cache-control">>, <<"no-cache">>}], + {ok, Req2} = cowboy_req:chunked_reply(200, Headers, Req1), + shotgun_test_utils:auto_send(count), + {loop, Req2, 1}; + {_, _} -> + Headers = [{<<"content-type">>, <<"text/html">>}], + StatusCode = 405, % Method not Allowed + cowboy_req:reply(StatusCode, Headers, Req), + {shutdown, Req, 0} + end. + +-spec info(term(), cowboy_req:req(), integer()) -> + {loop, cowboy_req:req(), integer()}. +info(count, Req, Count) -> + case Count > 2 of + true -> + {ok, Req, 0}; + false -> + cowboy_req:chunk(integer_to_binary(Count), Req), + shotgun_test_utils:auto_send(count), + {loop, Req, Count + 1} + end. + + +-spec terminate(term(), cowboy_req:req(), integer()) -> ok. +terminate(_Reason, _Req, _State) -> + ok. diff --git a/test/http_server/http_server.erl b/test/http_server/http_server.erl new file mode 100644 index 0000000..a2f5aca --- /dev/null +++ b/test/http_server/http_server.erl @@ -0,0 +1,63 @@ +-module(http_server). + +-export([ start/0 + , stop/0 + ]). + +-export([ start/2 + , stop/1 + , start_phase/3 + ]). + +%%------------------------------------------------------------------------------ +%% Application +%%------------------------------------------------------------------------------ + +%% @doc Starts the application +start() -> + application:ensure_all_started(?MODULE). + +%% @doc Stops the application +stop() -> + application:stop(?MODULE). + +%%------------------------------------------------------------------------------ +%% Behaviour +%%------------------------------------------------------------------------------ + +%% @private +start(_StartType, _StartArgs) -> + http_server_sup:start_link(). + +%% @private +stop(_State) -> + ok = cowboy:stop_listener(http_server). + +-spec start_phase(atom(), application:start_type(), []) -> ok | {error, term()}. +start_phase(start_cowboy_http, _StartType, []) -> + Port = application:get_env(http_server, http_port, 8888), + ListenerCount = application:get_env(http_server, http_listener_count, 10), + Routes = + [{ '_' + , [ {"/", http_simple_handler, []} + , {"/basic-auth", http_basic_auth_handler, []} + + , {"/chunked-sse[/:count]", lasse_handler, [http_sse_handler]} + , {"/chunked-binary", http_binary_handler, []} + ] + } + ], + Dispatch = cowboy_router:compile(Routes), + RanchOptions = [{port, Port}], + CowboyOptions = + [ + {env, + [ + {dispatch, Dispatch} + ]}, + {compress, true}, + {timeout, 12000} + ], + {ok, _} = + cowboy:start_http(http_server, ListenerCount, RanchOptions, CowboyOptions), + ok. diff --git a/test/http_server/http_server_sup.erl b/test/http_server/http_server_sup.erl new file mode 100644 index 0000000..6b5e79c --- /dev/null +++ b/test/http_server/http_server_sup.erl @@ -0,0 +1,14 @@ +-module(http_server_sup). + +-behaviour(supervisor). + +-export([start_link/0]). +-export([init/1]). + +%% admin api +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, {}). + +%% behaviour callbacks +init({}) -> + {ok, {{one_for_one, 5, 10}, []} }. diff --git a/test/http_server/http_simple_handler.erl b/test/http_server/http_simple_handler.erl new file mode 100644 index 0000000..d9163cf --- /dev/null +++ b/test/http_server/http_simple_handler.erl @@ -0,0 +1,53 @@ +-module(http_simple_handler). + +-include_lib("mixer/include/mixer.hrl"). +-mixin([ + {http_base_handler, + [ + init/3, + rest_init/2, + content_types_accepted/2, + content_types_provided/2, + resource_exists/2 + ]} + ]). + +-export([ allowed_methods/2 + , handle_head/2 + , handle_get/2 + , handle_post/2 + , delete_resource/2 + ]). + +%% cowboy +allowed_methods(Req, State) -> + Methods =[ <<"GET">> + , <<"POST">> + , <<"HEAD">> + , <<"OPTIONS">> + , <<"DELETE">> + , <<"PATCH">> + , <<"PUT">> + ], + {Methods, Req, State}. + +%% internal + +handle_head(Req, State) -> + {<<>>, Req, State}. + +handle_get(Req, State) -> + %% Force cowboy to send more than one TCP packet by making + %% the body big enough + Body = lists:duplicate(1000, <<"I'm simple!">>), + {Body, Req, State}. + +handle_post(Req, State) -> + {ok, Data, Req1} = cowboy_req:body(Req), + {Method, Req2} = cowboy_req:method(Req1), + Body = [Method, <<": ">>, Data], + Req3 = cowboy_req:set_resp_body(Body, Req2), + {true, Req3, State}. + +delete_resource(Req, State) -> + {true, Req, State}. diff --git a/test/http_server/http_sse_handler.erl b/test/http_server/http_sse_handler.erl new file mode 100644 index 0000000..57fc96d --- /dev/null +++ b/test/http_server/http_sse_handler.erl @@ -0,0 +1,39 @@ +-module(http_sse_handler). +-behavior(lasse_handler). + +-dialyzer(no_undefined_callbacks). + +-export([ + init/3, + handle_notify/2, + handle_info/2, + handle_error/3, + terminate/3 + ]). + +init(_InitArgs, _LastEventId, Req) -> + shotgun_test_utils:auto_send(ping), + {CountBin, Req1} = cowboy_req:binding(count, Req, <<"2">>), + case binary_to_integer(CountBin) of + 0 -> {no_content, Req1, {0, 0}}; + Count -> {ok, Req1, {1, Count + 1}} + end. + +handle_notify(ping, State) -> + {nosend, State}. + +handle_info(ping, {X, Count} = State) when X >= Count -> + {stop, State}; +handle_info(ping, {X, Count}) -> + shotgun_test_utils:auto_send(ping), + + Event = #{ id => integer_to_binary(X) + , event => <<"ping-pong">> + , data => <<"pong">> + , comment => <<"This is a comment">> + }, + {send, Event, {X + 1, Count}}. + +handle_error(_Msg, _Reason, Count) -> Count. + +terminate(_Reason, _Req, _Count) -> ok. diff --git a/test/shotgun_SUITE.erl b/test/shotgun_SUITE.erl new file mode 100644 index 0000000..328d005 --- /dev/null +++ b/test/shotgun_SUITE.erl @@ -0,0 +1,199 @@ +-module(shotgun_SUITE). + +-export([ all/0 + , init_per_suite/1 + , end_per_suite/1 + , init_per_testcase/2 + , end_per_testcase/2 + ]). + +-export([ open/1 + , basic_auth/1 + , get/1 + , post/1 + , delete/1 + , head/1 + , options/1 + , patch/1 + , put/1 + , missing_slash_uri/1 + , complete_coverage/1 + ]). + +-include_lib("common_test/include/ct.hrl"). + +%%------------------------------------------------------------------------------ +%% Common Test +%%------------------------------------------------------------------------------ + +-spec all() -> [atom()]. +all() -> shotgun_test_utils:all(?MODULE). + +-spec init_per_suite(shotgun_test_utils:config()) -> + shotgun_test_utils:config(). +init_per_suite(Config) -> + {ok, _} = shotgun:start(), + {ok, _} = http_server:start(), + Config. + +-spec end_per_suite(shotgun_test_utils:config()) -> shotgun_test_utils:config(). +end_per_suite(Config) -> + ok = shotgun:stop(), + ok = http_server:stop(), + Config. + +-spec init_per_testcase(atom(), shotgun_test_utils:config()) -> + shotgun_test_utils:config(). +init_per_testcase(_, Config) -> + Port = application:get_env(http_server, http_port, 8888), + {ok, Conn} = shotgun:open("localhost", Port), + [{conn, Conn} | Config]. + +-spec end_per_testcase(atom(), shotgun_test_utils:config()) -> + shotgun_test_utils:config(). +end_per_testcase(_, Config) -> + Conn = ?config(conn, Config), + ok = shotgun:close(Conn), + Config. + +%%------------------------------------------------------------------------------ +%% Test Cases +%%------------------------------------------------------------------------------ + +-spec open(shotgun_test_utils:config()) -> {comment, string()}. +open(_Config) -> + {error, gun_open_failed} = shotgun:open("whatever", 8888), + + {error, gun_open_timeout} = shotgun:open("google.com", 8888, #{timeout => 1}), + + {ok, Conn} = shotgun:open("localhost", 8888), + ok = shotgun:close(Conn), + + {comment, ""}. + +-spec basic_auth(shotgun_test_utils:config()) -> {comment, string()}. +basic_auth(Config) -> + Conn = ?config(conn, Config), + + ct:comment("GET should return 401"), + {ok, Response1} = shotgun:get(Conn, "/basic-auth"), + #{status_code := 401} = Response1, + + ct:comment("GET should return 200"), + Headers = #{basic_auth => {"user", "pass"}}, + {ok, Response2} = shotgun:get(Conn, "/basic-auth", Headers), + #{status_code := 200} = Response2, + + {comment, ""}. + +-spec get(shotgun_test_utils:config()) -> {comment, string()}. +get(Config) -> + Conn = ?config(conn, Config), + + ct:comment("GET should return 200"), + {ok, Response} = shotgun:get(Conn, "/"), + #{status_code := 200} = Response, + + {ok, Response} = shotgun:get(Conn, "/", #{}), + #{status_code := 200} = Response, + + {comment, ""}. + +-spec post(shotgun_test_utils:config()) -> {comment, string()}. +post(Config) -> + Conn = ?config(conn, Config), + + ct:comment("POST should return 200"), + Headers = #{<<"content-type">> => <<"text/plain">>}, + {ok, Response} = shotgun:post(Conn, "/", Headers, <<"Hello">>, #{}), + #{ status_code := 200 + , body := <<"POST: Hello">> + } = Response, + + {comment, ""}. + +-spec delete(shotgun_test_utils:config()) -> {comment, string()}. +delete(Config) -> + Conn = ?config(conn, Config), + + ct:comment("DELETE should return 204"), + Headers = #{<<"content-type">> => <<"text/plain">>}, + {ok, Response} = shotgun:delete(Conn, "/", Headers, #{}), + #{status_code := 204} = Response, + + {comment, ""}. + +-spec head(shotgun_test_utils:config()) -> {comment, string()}. +head(Config) -> + Conn = ?config(conn, Config), + + ct:comment("HEAD should return 200"), + Headers = #{<<"content-type">> => <<"text/plain">>}, + {ok, Response} = shotgun:head(Conn, "/", Headers, #{}), + #{status_code := 200} = Response, + + {comment, ""}. + +-spec options(shotgun_test_utils:config()) -> {comment, string()}. +options(Config) -> + Conn = ?config(conn, Config), + + ct:comment("OPTIONS should return 200"), + Headers = #{<<"content-type">> => <<"text/plain">>}, + {ok, Response} = shotgun:options(Conn, "/", Headers, #{}), + #{status_code := 200} = Response, + + {comment, ""}. + +-spec patch(shotgun_test_utils:config()) -> {comment, string()}. +patch(Config) -> + Conn = ?config(conn, Config), + + ct:comment("PATCH should return 200"), + Headers = #{<<"content-type">> => <<"text/plain">>}, + + {ok, Response} = shotgun:patch(Conn, "/", Headers, <<"Hello">>, #{}), + #{ status_code := 200 + , body := <<"PATCH: Hello">> + } = Response, + + {comment, ""}. + +-spec put(shotgun_test_utils:config()) -> {comment, string()}. +put(Config) -> + Conn = ?config(conn, Config), + + ct:comment("PUT should return 200"), + Headers = #{<<"content-type">> => <<"text/plain">>}, + + {ok, Response} = shotgun:put(Conn, "/", Headers, <<"Hello">>, #{}), + #{ status_code := 200 + , body := <<"PUT: Hello">> + } = Response, + + {comment, ""}. + +-spec missing_slash_uri(shotgun_test_utils:config()) -> {comment, string()}. +missing_slash_uri(Config) -> + Conn = ?config(conn, Config), + + ct:comment("GET should return error"), + {error, missing_slash_uri} = shotgun:get(Conn, "hello"), + + ct:comment("POST should return error"), + {error, missing_slash_uri} = shotgun:post(Conn, "hello", #{}, <<>>, #{}), + + {comment, ""}. + +-spec complete_coverage(shotgun_test_utils:config()) -> {comment, string()}. +complete_coverage(Config) -> + Conn = ?config(conn, Config), + + ct:comment("Sending unexpected events should return an error"), + {error, {unexpected, whatever}} = gen_fsm:sync_send_event(Conn, whatever), + {error, {unexpected, _}} = shotgun:data(Conn, <<"data">>), + + ct:comment("gen_server's code_change"), + {ok, at_rest, #{}} = shotgun:code_change(old_vsn, at_rest, #{}, extra), + + {comment, ""}. diff --git a/test/shotgun_async_SUITE.erl b/test/shotgun_async_SUITE.erl new file mode 100644 index 0000000..eca385a --- /dev/null +++ b/test/shotgun_async_SUITE.erl @@ -0,0 +1,180 @@ +-module(shotgun_async_SUITE). + +-export([ all/0 + , init_per_suite/1 + , end_per_suite/1 + , init_per_testcase/2 + , end_per_testcase/2 + ]). + +-export([ get_sse/1 + , get_binary/1 + , work_queue/1 + , get_handle_event/1 + , async_unsupported/1 + ]). + +-include_lib("common_test/include/ct.hrl"). + +%%------------------------------------------------------------------------------ +%% Common Test +%%------------------------------------------------------------------------------ + +-spec all() -> [atom()]. +all() -> shotgun_test_utils:all(?MODULE). + +-spec init_per_suite(shotgun_test_utils:config()) -> + shotgun_test_utils:config(). +init_per_suite(Config) -> + {ok, _} = shotgun:start(), + {ok, _} = http_server:start(), + Config. + +-spec end_per_suite(shotgun_test_utils:config()) -> + shotgun_test_utils:config(). +end_per_suite(Config) -> + ok = shotgun:stop(), + ok = http_server:stop(), + Config. + +-spec init_per_testcase(atom(), shotgun_test_utils:config()) -> + shotgun_test_utils:config(). +init_per_testcase(_, Config) -> + Port = application:get_env(http_server, http_port, 8888), + {ok, Conn} = shotgun:open("localhost", Port), + [{conn, Conn} | Config]. + +-spec end_per_testcase(atom(), shotgun_test_utils:config()) -> + shotgun_test_utils:config(). +end_per_testcase(_, Config) -> + Conn = ?config(conn, Config), + ok = shotgun:close(Conn), + Config. + +%%------------------------------------------------------------------------------ +%% Test Cases +%%------------------------------------------------------------------------------ + +-spec get_sse(shotgun_test_utils:config()) -> {comment, string()}. +get_sse(Config) -> + Conn = ?config(conn, Config), + + ct:comment("GET returns a ref"), + Opts = #{async => true, async_mode => sse}, + {ok, Ref} = shotgun:get(Conn, <<"/chunked-sse">>, #{}, Opts), + true = is_reference(Ref), + + timer:sleep(500), + + ct:comment("There are 3 elements"), + [Event1, Event2, Fin] = shotgun:events(Conn), + + {nofin, Ref, EventBin1} = Event1, + #{data := <<"pong\n">>} = shotgun:parse_event(EventBin1), + {nofin, Ref, EventBin2} = Event2, + #{data := <<"pong\n">>} = shotgun:parse_event(EventBin2), + {fin, Ref, <<>>} = Fin, + + ct:comment("GET returns a response when no content is available"), + {ok, Response} = shotgun:get(Conn, <<"/chunked-sse/0">>, #{}, Opts), + #{status_code := 204} = Response, + + ct:comment("There are 0 elements"), + [] = shotgun:events(Conn), + + {comment, ""}. + +-spec get_binary(shotgun_test_utils:config()) -> {comment, string()}. +get_binary(Config) -> + Conn = ?config(conn, Config), + + ct:comment("GET should return a ref"), + Opts = #{async => true, async_mode => binary}, + {ok, Ref} = shotgun:get(Conn, <<"/chunked-binary">>, #{}, Opts), + true = is_reference(Ref), + + timer:sleep(500), + + [Chunk1, Chunk2, Fin] = shotgun:events(Conn), + + {nofin, Ref, <<"1">>} = Chunk1, + {nofin, Ref, <<"2">>} = Chunk2, + {fin, Ref, <<>>} = Fin, + + {comment, ""}. + +-spec work_queue(shotgun_test_utils:config()) -> {comment, string()}. +work_queue(Config) -> + Conn = ?config(conn, Config), + + ct:comment("Async GET should return a ref"), + Opts = #{async => true, async_mode => sse}, + {ok, RefAsyncGet} = shotgun:get(Conn, <<"/chunked-sse/20">>, #{}, Opts), + true = is_reference(RefAsyncGet), + + ct:comment("Queued GET should return a ref as well"), + {ok, Response} = shotgun:get(Conn, <<"/">>), + #{status_code := 200} = Response, + + ct:comment("Events frmo the async GET should be there"), + Events = shotgun:events(Conn), + 21 = length(Events), %% 20 nofin + 1 fin + + {comment, ""}. + +-spec get_handle_event(shotgun_test_utils:config()) -> {comment, string()}. +get_handle_event(Config) -> + Conn = ?config(conn, Config), + Self = self(), + + ct:comment("SSE: GET should return a ref"), + HandleEvent = fun(_, _, EventBin) -> + case shotgun:parse_event(EventBin) of + #{id := Data} -> Self ! Data; + _ -> ok + end + end, + Opts = #{ async => true + , async_mode => sse + , handle_event => HandleEvent + }, + {ok, _Ref} = shotgun:get(Conn, <<"/chunked-sse/3">>, #{}, Opts), + + timer:sleep(500), + + ok = shotgun_test_utils:wait_receive(<<"1">>, 500), + ok = shotgun_test_utils:wait_receive(<<"2">>, 500), + ok = shotgun_test_utils:wait_receive(<<"3">>, 500), + timeout = shotgun_test_utils:wait_receive(<<"4">>, 500), + + ct:comment("SSE: GET should return a ref"), + HandleEventBin = fun(_, _, Data) -> Self ! Data end, + OptsBin = #{ async => true + , async_mode => binary + , handle_event => HandleEventBin + }, + {ok, _RefBin} = shotgun:get(Conn, <<"/chunked-binary">>, #{}, OptsBin), + + timer:sleep(500), + + ok = shotgun_test_utils:wait_receive(<<"1">>, 500), + ok = shotgun_test_utils:wait_receive(<<"2">>, 500), + timeout = shotgun_test_utils:wait_receive(<<"3">>, 500), + + {comment, ""}. + +-spec async_unsupported(shotgun_test_utils:config()) -> {comment, string()}. +async_unsupported(Config) -> + Conn = ?config(conn, Config), + + ct:comment("Async POST should return an error"), + { error + , {async_unsupported, post} + } = shotgun:post(Conn, "/", #{}, <<>>, #{async => true}), + + ct:comment("Async PUT should return an error"), + { error + , {async_unsupported, put} + } = shotgun:put(Conn, "/", #{}, <<>>, #{async => true}), + + {comment, ""}. diff --git a/test/shotgun_meta_SUITE.erl b/test/shotgun_meta_SUITE.erl new file mode 100644 index 0000000..87dcce5 --- /dev/null +++ b/test/shotgun_meta_SUITE.erl @@ -0,0 +1,8 @@ +-module(shotgun_meta_SUITE). + +-include_lib("mixer/include/mixer.hrl"). +-mixin([ktn_meta_SUITE]). + +-export([init_per_suite/1]). + +init_per_suite(Config) -> [{application, shotgun} | Config]. diff --git a/test/shotgun_test_utils.erl b/test/shotgun_test_utils.erl new file mode 100644 index 0000000..c46e2df --- /dev/null +++ b/test/shotgun_test_utils.erl @@ -0,0 +1,25 @@ +-module(shotgun_test_utils). + +-type config() :: [{atom(), term()}]. +-export_type([config/0]). + +-export([all/1]). + +-export([ auto_send/1 + , wait_receive/2 + ]). + +-spec all(atom()) -> [atom()]. +all(Module) -> + ExcludedFuns = [module_info, init_per_suite, end_per_suite], + [F || {F, 1} <- Module:module_info(exports)] -- ExcludedFuns. + +-spec auto_send(any()) -> reference(). +auto_send(Msg) -> + erlang:send_after(100, self(), Msg). + +-spec wait_receive(any(), timeout()) -> ok | timeout. +wait_receive(Value, Timeout) -> + receive Value -> ok + after Timeout -> timeout + end.