├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── package.config ├── package.exs ├── rebar ├── rebar.config ├── src ├── erl_streams.app.src ├── erl_streams_commons.hrl ├── gen_stream.erl └── stream.erl └── tests ├── add2_stream.erl ├── etap.erl ├── gen_stream.t ├── multi2_stream.erl ├── pipe_test.t ├── simple_stream.erl ├── simple_stream.t └── stream.t /.coveralls.yml: -------------------------------------------------------------------------------- 1 | # .coveralls.yml 2 | service_name: travis-ci 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | ebin 8 | rel/example_project 9 | .concrete/DEV_MODE 10 | .rebar 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | script: "make && make test" 3 | otp_release: 4 | - R15B03 5 | - R16B 6 | - R16B01 7 | - R16B02 8 | - R16B03 9 | - R16B03-1 10 | - 17.0 11 | - 17.1 12 | - 17.3 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Evangelos Pappas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ERL ?= erl 2 | ERLC ?= erlc 3 | APP := erl_streams 4 | REBAR ?= ./rebar 5 | 6 | all: deps compile test doc 7 | 8 | deps: 9 | @$(REBAR) -C rebar.config get-deps 10 | 11 | compile: 12 | @$(REBAR) -C rebar.config compile 13 | 14 | test: 15 | @$(ERLC) -o tests/ tests/*.erl 16 | prove -v tests/*.t 17 | 18 | doc: 19 | $(REBAR) -C rebar.config doc skip_deps=true 20 | 21 | cover: all 22 | COVER=1 prove tests/*.t 23 | @$(ERL) -detached -noshell -eval 'etap_report:create()' -s init stop 24 | 25 | clean: 26 | @$(REBAR) clean 27 | @rm -f bin/*.beam 28 | @rm -f tests/*.beam 29 | @rm -f doc/*.html doc/*.css doc/edoc-info doc/*.png 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # erlang_streams 2 | 3 | [![Build Status](https://travis-ci.org/epappas/erl_streams.svg)](https://travis-ci.org/epappas/erl_streams) 4 | 5 | A Stream wrapper library 6 | 7 | # install 8 | 9 | include it as a rebar dependency: 10 | 11 | {erl_streams, "*", {git, "git://github.com/epappas/erl_streams.git", {branch, "master"}}} 12 | 13 | # Idea 14 | 15 | ## As process wrapper 16 | 17 | ### gen_stream 18 | 19 | A generic stream generator, a smart queue with states and able to pause or drop on backpressure. 20 | 21 | Example: 22 | 23 | %% Initiate Simple Stream 24 | {ok, StreamPID} = gen_stream:start(test), 25 | 26 | %% Simple put & take 27 | ok = gen_stream:put(StreamPID, 1), 28 | {ok, 1} = gen_stream:take(StreamPID), 29 | 30 | %% Pause state 31 | ok = gen_stream:pause(StreamPID), 32 | {error, pause} = gen_stream:put(StreamPID, test), 33 | ok = gen_stream:drain(StreamPID), 34 | ok = gen_stream:put(StreamPID, 1), 35 | {ok, 1} = gen_stream:take_and_pause(StreamPID), 36 | true = gen_stream:is_paused(StreamPID), 37 | 38 | %% Set a Max size, to manage back pressure 39 | {ok, StreamPID} = gen_stream:start(test, 1), 40 | ok = gen_stream:put(StreamPID, test), 41 | {error, pause} = gen_stream:put(StreamPID, test), 42 | {ok, test} = gen_stream:take(StreamPID), 43 | ok = gen_stream:put(StreamPID, test), 44 | 45 | %% Pipe through resources 46 | 47 | %% add2_stream 48 | 49 | -module(add2_stream). 50 | 51 | -behaviour(gen_stream). 52 | 53 | %% gen_stream callbacks 54 | -export([init/1, on_data/3, on_offer/3, on_state/3]). 55 | 56 | init(_Args) -> {ok, {}}. 57 | 58 | on_data(_Resource, Stream, State) -> {ok, Stream, State}. 59 | 60 | on_offer(Resource, Stream, State) -> {Resource + 2, Stream, State}. 61 | 62 | on_state(State, _Stream, StateData) -> {ok, StateData}. 63 | 64 | %% multi2_stream 65 | 66 | -module(multi2_stream). 67 | 68 | -behaviour(gen_stream). 69 | 70 | %% gen_stream callbacks 71 | -export([init/1, on_data/3, on_offer/3, on_state/3]). 72 | 73 | init(_Args) -> {ok, {}}. 74 | 75 | on_data(_Resource, Stream, State) -> {ok, Stream, State}. 76 | 77 | on_offer(Resource, Stream, State) -> {Resource * 2, Stream, State}. 78 | 79 | on_state(State, _Stream, StateData) -> {ok, StateData}. 80 | 81 | 82 | %% main... 83 | 84 | {ok, AddStreamPID} = gen_stream:start(add2_stream, add2_stream, []), 85 | {ok, MultiStream1PID} = gen_stream:start(multi2_stream, multi2_stream, []), 86 | {ok, MultiStream2PID} = gen_stream:start(multi2_stream, multi2_stream, []), 87 | 88 | gen_stream:pipe(AddStreamPID, MultiStream1PID), 89 | gen_stream:pipe(MultiStream1PID, MultiStream2PID), 90 | 91 | gen_stream:put(AddStreamPID, 3), 92 | 93 | {ok, 20} = gen_stream:take(MultiStream2PID). 94 | 95 | 96 | #### Methods 97 | 98 | ##### gen_stream:start/0, 99 | 100 | Creates an anonymous stream 101 | 102 | gen_stream:start() -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()} 103 | 104 | ##### gen_stream:start/1, 105 | 106 | Creates a Named stream with default settings 107 | 108 | gen_stream:start(any()) -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()} 109 | 110 | ##### gen_stream:start/2, 111 | 112 | Creates a Named stream with Max buffer or wraps a stream that implements the `gen_stream` behaviour 113 | 114 | gen_stream:start(any(), number() | atom()) -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()} 115 | 116 | {ok, Pid} = gen_stream:start(test, 10), 117 | {ok, Pid} = gen_stream:start(test, simple_stream) 118 | 119 | ##### gen_stream:start/3, 120 | 121 | gen_stream:start(any(), number() | atom(), list() | number()) -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()} 122 | 123 | {ok, Pid} = gen_stream:start(test, simple_stream, 10), 124 | {ok, Pid} = gen_stream:start(test, simple_stream, InitArgs) 125 | 126 | ##### gen_stream:start/4, 127 | 128 | gen_stream:start(any(), number(), atom(), list()) -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()} 129 | 130 | {ok, Pid} = gen_stream:start(test, 10, simple_stream, InitArgs) 131 | 132 | ##### gen_stream:pipe/2, 133 | 134 | gen_stream:pipe(pid(), pid()) -> ok 135 | 136 | ok = gen_stream:pipe(Stream_left_PID, Stream_right_PID) 137 | 138 | ##### gen_stream:put/2, 139 | 140 | gen_stream:put(pid(), any()) -> ok | {error, pause} 141 | 142 | ##### gen_stream:put_from_list/2, 143 | 144 | gen_stream:put_from_list(pid(), list()) -> ok | {error, pause} 145 | 146 | ##### gen_stream:put_while/2, 147 | 148 | gen_stream:put_while(pid(), fun()) -> ok | {error, pause} 149 | 150 | where fun() 151 | 152 | fun (_StreamPid) -> 153 | Resource 154 | end 155 | 156 | ##### gen_stream:take/1, 157 | 158 | gen_stream:take(pid()) -> {ok, any()} 159 | 160 | Take gives `undefined` when stream is empty 161 | 162 | ##### gen_stream:take/2, 163 | 164 | Take a number of resources 165 | 166 | gen_stream:take(pid(), number()) -> {ok, any()} 167 | 168 | ##### gen_stream:take_and_pause/2, 169 | 170 | The stream will be paused after this call 171 | 172 | gen_stream:take_and_pause(pid()) -> {ok, any()} 173 | 174 | ##### gen_stream:drain/1, 175 | 176 | Unpause the stream, if the stream is blocked due to back pressure, then drain is not affecting 177 | 178 | gen_stream:drain(pid()) -> ok 179 | 180 | ##### gen_stream:resume/1, 181 | 182 | Stream will no drop anymore 183 | 184 | gen_stream:resume(pid()) -> ok 185 | 186 | ##### gen_stream:drop/1, 187 | 188 | Streams is in a drop state, each inputted resource will be ignored, and the response will be `ok` 189 | 190 | gen_stream:drop(pid()) -> ok 191 | 192 | ##### gen_stream:drop_while/2, 193 | 194 | gen_stream:drop_while(pid(), fun()) -> ok 195 | 196 | Where `fun/2` 197 | 198 | Dropping_Fn(StreamPid, Resource) -> boolean() 199 | 200 | ##### gen_stream:pause/1, 201 | 202 | gen_stream:pause(pid()) -> ok 203 | 204 | ##### gen_stream:filter/2, 205 | 206 | gen_stream:filter(pid(), fun()) -> ok 207 | 208 | Where `fun/2` 209 | 210 | FilterFn(Resource, Buffer) -> boolean() 211 | 212 | ##### gen_stream:map/2, 213 | 214 | gen_stream:map(pid(), fun()) -> ok 215 | 216 | Where `fun/2` 217 | 218 | MapFn(Resource, Buffer) -> Resource :: any() 219 | 220 | ##### gen_stream:reduce/2, 221 | 222 | gen_stream:reduce(pid(), fun()) -> ok 223 | 224 | Where `fun/2` 225 | 226 | ReduceFn(Acc, Resource, Buffer) -> NewAcc :: any() 227 | 228 | ##### gen_stream:reduce/3, 229 | 230 | gen_stream:reduce(pid(), fun(), Acc :: any()) -> ok 231 | 232 | Where `fun/2` 233 | 234 | ReduceFn(Acc, Resource, Buffer) -> NewAcc :: any() 235 | 236 | ##### gen_stream:can_accept/1, 237 | 238 | gen_stream:can_accept(pid()) -> boolean() 239 | 240 | ##### gen_stream:is_empty/1, 241 | 242 | gen_stream:is_empty(pid()) -> boolean() 243 | 244 | ##### gen_stream:is_paused/1, 245 | 246 | gen_stream:is_paused(pid()) -> boolean() 247 | 248 | ##### gen_stream:is_closed/1, 249 | 250 | gen_stream:is_closed(pid()) -> boolean() 251 | 252 | ##### gen_stream:is_stopped/1, 253 | 254 | gen_stream:is_stopped(pid()) -> boolean() 255 | 256 | ##### gen_stream:is_open/1, 257 | 258 | gen_stream:is_open(pid()) -> boolean() 259 | 260 | 261 | #### As a data structure 262 | 263 | ###### `#stream{}` 264 | 265 | A data structure with cases to handle backpressure, paused and closed states 266 | -------------------------------------------------------------------------------- /package.config: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% @author Evangelos Pappas 4 | %%% @copyright (C) 2015, evalonlabs 5 | %%% The MIT License (MIT) 6 | %%% 7 | %%% Copyright (c) 2015 Evangelos Pappas 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in all 17 | %%% copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | %%% SOFTWARE. 26 | %%% @doc 27 | %%% 28 | %%% @end 29 | %%%------------------------------------------------------------------- 30 | 31 | [ 32 | {name, "erl_streams"}, 33 | {descriptio, "Streams in Erlang"}, 34 | {version, "0.5.0"}, 35 | {keywords, ["streams"]}, 36 | {dependencies, []}, 37 | {licenses, ["MIT"]}, 38 | {author, "Evangelos Pappas "}, 39 | {contributors, [ 40 | [{name, "Evangelos Pappas"}, {email, "epappas@evalonlabs.com"}] 41 | ]}, 42 | {maintainers, [ 43 | [{name, "Evangelos Pappas"}, {email, "epappas@evalonlabs.com"}] 44 | ]}, 45 | {repository, [{type, git}, {url, "https://github.com/epappas/erl_streams.git"}]}, 46 | {homepage, "https://github.com/epappas/erl_streams"} 47 | ]. 48 | -------------------------------------------------------------------------------- /package.exs: -------------------------------------------------------------------------------- 1 | Expm.Package.new( 2 | name: "erl_streams", 3 | description: "Streams in Erlang", 4 | version: "0.5.0", 5 | keywords: ["streams"], 6 | dependencies: [], 7 | licenses: [[name: "MIT", file: "LICENSE"]], 8 | contributors: [], 9 | maintainers: [ 10 | [name: "Evangelos Pappas", email: "epappas@evalonlabs.com"] 11 | ], 12 | repositories: [[github: "epappas/erl_streams", tag: "0.5.0"]] 13 | ) 14 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epappas/erl_streams/247b9ffd0f51c398fb23d8f87ee4457aec2b8fe0/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, fail_on_warning, {d, 'WITH_JIFFY'}]}. 2 | 3 | {deps_dir, "deps"}. 4 | {deps, []}. 5 | {cover_enabled, true}. 6 | {eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}. -------------------------------------------------------------------------------- /src/erl_streams.app.src: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% @author Evangelos Pappas 4 | %%% @copyright (C) 2015, evalonlabs 5 | %%% The MIT License (MIT) 6 | %%% 7 | %%% Copyright (c) 2015 Evangelos Pappas 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in all 17 | %%% copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | %%% SOFTWARE. 26 | %%% @doc 27 | %%% 28 | %%% @end 29 | %%%------------------------------------------------------------------- 30 | 31 | {application, erl_streams, [ 32 | {description, "Streams in Erlang"}, 33 | {vsn, "0.5.0"}, 34 | {modules, [ 35 | stream, 36 | gen_stream 37 | ]}, 38 | {registered, [ 39 | stream, 40 | gen_stream 41 | ]}, 42 | {applications, [ 43 | kernel, 44 | stdlib 45 | ]}, 46 | {env, []} 47 | ]}. 48 | -------------------------------------------------------------------------------- /src/erl_streams_commons.hrl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% @author Evangelos Pappas 4 | %%% @copyright (C) 2015, evalonlabs 5 | %%% The MIT License (MIT) 6 | %%% 7 | %%% Copyright (c) 2015 Evangelos Pappas 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in all 17 | %%% copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | %%% SOFTWARE. 26 | %%% @doc 27 | %%% 28 | %%% @end 29 | %%%------------------------------------------------------------------- 30 | 31 | -author("evangelosp"). 32 | 33 | -record(stream, { 34 | name, 35 | mod = undefined, 36 | mod_state = undefined, 37 | buffer = [], 38 | pipes = [], 39 | pre_waterfall = [], 40 | post_waterfall = [], 41 | reduce_acc = undefined, 42 | is_closed = false, 43 | is_paused = false, 44 | is_stoped = false, 45 | is_dropping = false, 46 | dropping_fn, 47 | dropping_ctr = 0, 48 | max_buffer = 134217728 49 | }). 50 | 51 | -------------------------------------------------------------------------------- /src/gen_stream.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% @author Evangelos Pappas 4 | %%% @copyright (C) 2015, evalonlabs 5 | %%% The MIT License (MIT) 6 | %%% 7 | %%% Copyright (c) 2015 Evangelos Pappas 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in all 17 | %%% copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | %%% SOFTWARE. 26 | %%% @doc 27 | %%% 28 | %%% @end 29 | %%%------------------------------------------------------------------- 30 | 31 | -module(gen_stream). 32 | -author("epappas"). 33 | 34 | -behaviour(gen_fsm). 35 | 36 | -include("./erl_streams_commons.hrl"). 37 | 38 | %% API 39 | -export([ 40 | start/0, 41 | start/1, 42 | start/2, 43 | start/3, 44 | start/4, 45 | %% TODO start_link/0, 46 | %% TODO start_link/4, 47 | put/2, 48 | put_from_list/2, 49 | put_while/2, 50 | take/1, 51 | take/2, 52 | take_and_pause/1, 53 | %% TODO delay/1, 54 | %% TODO delay_while/1, 55 | drain/1, 56 | drop/1, 57 | drop_while/2, 58 | resume/1, 59 | pause/1, 60 | pipe/2, 61 | filter/2, 62 | map/2, 63 | reduce/2, 64 | reduce/3, 65 | %% TODO zip/2, 66 | can_accept/1, 67 | is_empty/1, 68 | is_paused/1, 69 | is_closed/1, 70 | is_stopped/1, 71 | is_open/1, 72 | get_stream/1 73 | ]). 74 | 75 | %% gen_fsm callbacks 76 | -export([ 77 | init/1, 78 | open/2, 79 | open/3, 80 | paused/2, 81 | paused/3, 82 | dropping/2, 83 | dropping/3, 84 | stopped/2, 85 | stopped/3, 86 | closed/2, 87 | closed/3, 88 | handle_event/3, 89 | handle_sync_event/4, 90 | handle_info/3, 91 | terminate/3, 92 | code_change/4 93 | ]). 94 | 95 | -define(SERVER, ?MODULE). 96 | 97 | %% States 98 | -define(OPEN, open). 99 | -define(DROPPING, dropping). 100 | -define(PAUSED, paused). 101 | -define(STOPPED, stopped). 102 | -define(CLOSED, closed). 103 | 104 | %%%=================================================================== 105 | %%% Interface functions. 106 | %%%=================================================================== 107 | 108 | -callback init(Args :: term()) -> 109 | {ok, StateData :: term()} | {stop, Reason :: term()}. 110 | 111 | -callback on_data(Resource :: any(), Stream :: #stream{}, State :: any()) -> 112 | {ignore, #stream{}, any()} | {ok, #stream{}, any()}. 113 | 114 | -callback on_offer(Resource :: any(), Stream :: #stream{}, State :: any()) -> 115 | {any(), #stream{}, any()}. 116 | 117 | -callback on_state(State :: atom(), Stream :: #stream{}, StateData :: any()) -> 118 | {ok, any()}. 119 | 120 | %%%=================================================================== 121 | %%% API 122 | %%%=================================================================== 123 | -spec(start() -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()}). 124 | start() -> 125 | gen_fsm:start(?MODULE, [], []). 126 | 127 | -spec(start(any()) -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()}). 128 | start(Name) -> 129 | gen_fsm:start(?MODULE, [{name, Name}], []). 130 | 131 | -spec(start(any(), number() | atom()) -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()}). 132 | start(Name, Max) when is_number(Max) -> 133 | gen_fsm:start(?MODULE, [{name, Name}, {max, Max}], []); 134 | 135 | start(Name, Mod) when is_atom(Mod) -> 136 | gen_fsm:start(?MODULE, [{name, Name}, {mod, Mod}], []). 137 | 138 | -spec(start(any(), number() | atom(), list() | number()) -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()}). 139 | start(Name, Mod, ModArgs) when is_list(ModArgs) andalso is_atom(Mod) -> 140 | gen_fsm:start(?MODULE, [{name, Name}, {mod, Mod}, {mod_args, ModArgs}], []); 141 | 142 | start(Name, Max, Mod) when is_number(Max) -> 143 | gen_fsm:start(?MODULE, [{name, Name}, {mod, Mod}, {max, Max}], []). 144 | 145 | -spec(start(any(), number(), atom(), list()) -> {ok, Pid} | {error, {already_started, Pid}} | {error, any()}). 146 | start(Name, Max, Mod, ModArgs) when is_list(ModArgs) -> 147 | gen_fsm:start(?MODULE, [{name, Name}, {mod, Mod}, {max, Max}, {mod_args, ModArgs}], []). 148 | 149 | -spec(put(pid(), any()) -> ok | {error, pause}). 150 | put(StreamPID, Resource) -> 151 | case gen_stream:can_accept(StreamPID) of 152 | true -> gen_fsm:send_event(StreamPID, {put, Resource}); 153 | false -> 154 | case gen_stream:is_paused(StreamPID) of 155 | true -> {error, pause}; 156 | false -> 157 | case gen_stream:is_stopped(StreamPID) of 158 | true -> {error, stopped}; 159 | false -> {error, closed} 160 | end 161 | end 162 | end. 163 | 164 | -spec(put_from_list(pid(), list()) -> ok | {error, pause}). 165 | put_from_list(_StreamPID, ResourceList) when ResourceList =:= [] -> ok; 166 | put_from_list(StreamPID, ResourceList) -> 167 | [H | T] = ResourceList, 168 | case gen_stream:put(StreamPID, H) of 169 | ok -> stream:put_from_list(StreamPID, T); 170 | OtherState -> OtherState %% closed, isPaused, or anything else 171 | end. 172 | 173 | -spec(put_while(pid(), any()) -> ok | {error, pause}). 174 | put_while(StreamPID, Fn) when is_function(Fn) -> 175 | case Fn(StreamPID) of 176 | undefined -> ok; 177 | Resource -> gen_stream:put(StreamPID, Resource) 178 | end. 179 | 180 | -spec(take(pid()) -> {ok, any()}). 181 | take(StreamPID) -> gen_fsm:sync_send_event(StreamPID, take). 182 | 183 | -spec(take(pid(), number()) -> {ok, any()}). 184 | take(StreamPID, Number) -> gen_fsm:sync_send_event(StreamPID, {take, Number}). 185 | 186 | -spec(take_and_pause(pid()) -> {ok, any()}). 187 | take_and_pause(StreamPID) -> gen_fsm:sync_send_event(StreamPID, take_and_pause). 188 | 189 | -spec(drain(pid()) -> ok). 190 | drain(StreamPID) -> gen_fsm:send_all_state_event(StreamPID, drain). 191 | 192 | -spec(resume(pid()) -> ok). 193 | resume(StreamPID) -> gen_fsm:send_all_state_event(StreamPID, resume). 194 | 195 | -spec(drop(pid()) -> ok). 196 | drop(StreamPID) -> gen_fsm:send_all_state_event(StreamPID, drop). 197 | 198 | -spec(drop_while(pid(), any()) -> ok). 199 | drop_while(StreamPID, Fn) -> gen_fsm:send_all_state_event(StreamPID, {drop_while, Fn}). 200 | 201 | -spec(pause(pid()) -> ok). 202 | pause(StreamPID) -> gen_fsm:send_all_state_event(StreamPID, pause). 203 | 204 | -spec(filter(pid(), any()) -> ok). 205 | filter(StreamPID, Fn) -> gen_fsm:send_all_state_event(StreamPID, {filter, Fn}). 206 | 207 | -spec(map(pid(), any()) -> ok). 208 | map(StreamPID, Fn) -> gen_fsm:send_all_state_event(StreamPID, {map, Fn}). 209 | 210 | -spec(reduce(pid(), any()) -> ok). 211 | reduce(StreamPID, Fn) -> gen_fsm:send_all_state_event(StreamPID, {reduce, Fn}). 212 | 213 | -spec(reduce(pid(), any(), any()) -> ok). 214 | reduce(StreamPID, Fn, Acc) -> gen_fsm:send_all_state_event(StreamPID, {reduce, Fn, Acc}). 215 | 216 | -spec(pipe(pid(), pid()) -> ok). 217 | pipe(StreamPID, NextStreamPID) -> gen_fsm:send_all_state_event(StreamPID, {pipe, NextStreamPID}). 218 | 219 | -spec(can_accept(pid()) -> boolean()). 220 | can_accept(StreamPID) -> gen_fsm:sync_send_all_state_event(StreamPID, can_accept). 221 | 222 | -spec(is_empty(pid()) -> boolean()). 223 | is_empty(StreamPID) -> gen_fsm:sync_send_all_state_event(StreamPID, is_empty). 224 | 225 | -spec(is_paused(pid()) -> boolean()). 226 | is_paused(StreamPID) -> gen_fsm:sync_send_all_state_event(StreamPID, is_paused). 227 | 228 | -spec(is_closed(pid()) -> boolean()). 229 | is_closed(StreamPID) -> gen_fsm:sync_send_all_state_event(StreamPID, is_closed). 230 | 231 | -spec(is_stopped(pid()) -> boolean()). 232 | is_stopped(StreamPID) -> gen_fsm:sync_send_all_state_event(StreamPID, is_stopped). 233 | 234 | -spec(is_open(pid()) -> boolean()). 235 | is_open(StreamPID) -> gen_fsm:sync_send_all_state_event(StreamPID, is_open). 236 | 237 | -spec(get_stream(pid()) -> #stream{}). 238 | get_stream(StreamPID) -> gen_fsm:sync_send_all_state_event(StreamPID, get_stream). 239 | 240 | %%%=================================================================== 241 | %%% gen_fsm callbacks 242 | %%%=================================================================== 243 | 244 | -spec(init(Args :: term()) -> 245 | {ok, StateName :: atom(), StateData :: #stream{}} | 246 | {ok, StateName :: atom(), StateData :: #stream{}, timeout() | hibernate} | 247 | {stop, Reason :: term()} | ignore). 248 | init([]) -> {ok, ?OPEN, stream:new()}; 249 | 250 | init(ArgsList) when is_list(ArgsList) -> 251 | Name = proplists:get_value(name, ArgsList, new_stream), 252 | Max = proplists:get_value(max, ArgsList, 134217728), 253 | Mod = proplists:get_value(mod, ArgsList, undefined), 254 | Mod_Args = proplists:get_value(mod_args, ArgsList, []), 255 | 256 | Stream = stream:new(Name, Max), 257 | 258 | case Mod of 259 | undefined -> {ok, ?OPEN, Stream#stream{max_buffer = Max, name = Name}}; 260 | Mod -> 261 | case Mod:init(Mod_Args) of 262 | {ok, StateData} -> 263 | {ok, ?OPEN, Stream#stream{mod = Mod, mod_state = StateData, max_buffer = Max, name = Name}}; 264 | {stop, Reason} -> 265 | {stop, Reason} 266 | end 267 | end. 268 | 269 | %% ========================================== 270 | %% OPEN STATE 271 | %% ========================================== 272 | 273 | open({put, _Resource}, #stream{is_paused = true} = Stream) -> next_state(?PAUSED, Stream); 274 | open({put, _Resource}, #stream{is_stoped = true} = Stream) -> next_state(?STOPPED, Stream); 275 | open({put, _Resource}, #stream{is_closed = true} = Stream) -> next_state(?CLOSED, Stream); 276 | 277 | open({put, Resource}, #stream{} = Stream) when is_list(Resource) -> 278 | {ok, NewStream} = stream:put_from_list(Stream, Resource), 279 | {next_state, ?OPEN, NewStream}; 280 | 281 | open({put, Fn}, #stream{} = Stream) when is_function(Fn) -> 282 | {ok, NewStream} = stream:put_while(Stream, Fn), 283 | {next_state, ?OPEN, NewStream}; 284 | 285 | open({put, Resource}, #stream{mod = undefined} = Stream) -> 286 | case stream:put(Stream, Resource) of 287 | {ok, NewStream} -> 288 | {next_state, ?OPEN, NewStream}; 289 | {pause, NewStream} -> 290 | {next_state, ?PAUSED, NewStream}; 291 | {stopped, NewStream} -> 292 | {next_state, ?STOPPED, NewStream}; 293 | {closed, NewStream} -> 294 | {next_state, ?CLOSED, NewStream} 295 | end; 296 | 297 | open({put, Resource}, #stream{mod = Mod, mod_state = StateData, pipes = []} = Stream) -> 298 | case Mod:on_data(Resource, Stream, StateData) of 299 | {ignore, MaybeNewStream, SD} -> {next_state, ?OPEN, MaybeNewStream#stream{mod_state = SD}}; 300 | {ok, MaybeNewStream, SD} -> 301 | case stream:put(MaybeNewStream#stream{mod_state = SD}, Resource) of 302 | {ok, NewStream} -> 303 | {next_state, ?OPEN, NewStream}; 304 | {pause, NewStream} -> 305 | next_state(?PAUSED, NewStream); 306 | {stopped, NewStream} -> 307 | next_state(?STOPPED, NewStream); 308 | {closed, NewStream} -> 309 | next_state(?CLOSED, NewStream) 310 | end 311 | end; 312 | 313 | open({put, Resource}, #stream{mod = Mod, mod_state = StateData, pipes = Pipes} = Stream) -> 314 | case Mod:on_data(Resource, Stream, StateData) of 315 | {ignore, MaybeNewStream, SD} -> {next_state, ?OPEN, MaybeNewStream#stream{mod_state = SD}}; 316 | {ok, MaybeNewStream, SD} -> 317 | {RSrc2, MaybeNewStream2, _} = Mod:on_offer(Resource, MaybeNewStream, SD), 318 | 319 | RecursionFn = fun(SelfFn, NextPipeList) -> 320 | case NextPipeList of 321 | [] -> ok; 322 | [NextStreamPID | RestPipeList] -> 323 | case NextStreamPID of 324 | undefined -> ok; 325 | _ -> 326 | case gen_stream:put(NextStreamPID, RSrc2) of 327 | ok -> SelfFn(SelfFn, RestPipeList); 328 | Err -> Err 329 | end 330 | end 331 | end 332 | end, 333 | 334 | case RecursionFn(RecursionFn, Pipes) of 335 | ok -> {next_state, ?OPEN, MaybeNewStream2}; 336 | _AnyOther -> next_state(?PAUSED, MaybeNewStream2) 337 | end 338 | end; 339 | 340 | open(_Event, #stream{} = State) -> {next_state, ?OPEN, State}. 341 | 342 | %% ===== Syncronous ===== 343 | 344 | open(_Event, _From, #stream{is_closed = true} = Stream) -> reply({error, closed}, ?CLOSED, Stream); 345 | 346 | open(_Event, _From, #stream{is_stoped = true} = Stream) -> reply({error, stopped}, ?STOPPED, Stream); 347 | 348 | open(take, _From, #stream{is_closed = false, mod = undefined} = Stream) -> 349 | {NewStream, Resource} = stream:take(Stream), 350 | {reply, {ok, Resource}, ?OPEN, NewStream}; 351 | 352 | open(take, _From, #stream{is_closed = false, mod = Mod, mod_state = StateData} = Stream) -> 353 | {NewStream, Resource} = stream:take(Stream), 354 | {RSrc, MaybeNewStream, SD} = Mod:on_offer(Resource, NewStream, StateData), 355 | {reply, {ok, RSrc}, ?OPEN, MaybeNewStream#stream{mod_state = SD}}; 356 | 357 | open(take_and_pause, _From, #stream{is_closed = false, mod = undefined} = Stream) -> 358 | {NewStream, Resource} = stream:take_and_pause(Stream), 359 | reply({ok, Resource}, ?PAUSED, NewStream); 360 | 361 | open(take_and_pause, _From, #stream{is_closed = false, mod = Mod, mod_state = StateData} = Stream) -> 362 | {NewStream, Resource} = stream:take_and_pause(Stream), 363 | {RSrc, MaybeNewStream, SD} = Mod:on_offer(Resource, NewStream, StateData), 364 | reply({ok, RSrc}, ?PAUSED, MaybeNewStream#stream{mod_state = SD}); 365 | 366 | open({take, Number}, _From, #stream{is_closed = false, mod = undefined} = Stream) -> 367 | {NewStream, ResourceList} = stream:take(Stream, Number), 368 | {reply, {ok, ResourceList}, ?OPEN, NewStream}; 369 | 370 | open({take, Number}, _From, #stream{is_closed = false, mod = Mod, mod_state = StateData} = Stream) -> 371 | {NewStream, ResourceList} = stream:take(Stream, Number), 372 | {RSrcList, MaybeNewStream, SD} = Mod:on_offer(ResourceList, NewStream, StateData), 373 | {reply, {ok, RSrcList}, ?OPEN, MaybeNewStream#stream{mod_state = SD}}; 374 | 375 | open(_Event, _From, State) -> {reply, {error, bad_call}, ?OPEN, State}. 376 | 377 | %% ========================================== 378 | %% PAUSED STATE 379 | %% ========================================== 380 | 381 | paused({put, _Resource}, Stream) -> {next_state, ?PAUSED, Stream}; 382 | 383 | paused(_Event, State) -> {next_state, ?PAUSED, State}. 384 | 385 | %% ===== Syncronous ===== 386 | 387 | paused(_Event, _From, #stream{is_closed = true} = Stream) -> reply({error, closed}, ?CLOSED, Stream); 388 | 389 | paused(_Event, _From, #stream{is_stoped = true} = Stream) -> reply({error, stopped}, ?STOPPED, Stream); 390 | 391 | paused(take, _From, #stream{is_closed = false, mod = undefined} = Stream) -> 392 | {NewStream, Resource} = stream:take(Stream), 393 | {reply, {ok, Resource}, ?PAUSED, NewStream}; 394 | 395 | paused(take, _From, #stream{is_closed = false, mod = Mod, mod_state = StateData} = Stream) -> 396 | {NewStream, Resource} = stream:take(Stream), 397 | {RSrc, MaybeNewStream, SD} = Mod:on_offer(Resource, NewStream, StateData), 398 | {reply, {ok, RSrc}, ?PAUSED, MaybeNewStream#stream{mod_state = SD}}; 399 | 400 | paused(take_and_pause, _From, #stream{is_closed = false, mod = undefined} = Stream) -> 401 | {NewStream, Resource} = stream:take_and_pause(Stream), 402 | {reply, {ok, Resource}, ?PAUSED, NewStream}; 403 | 404 | paused(take_and_pause, _From, #stream{is_closed = false, mod = Mod, mod_state = StateData} = Stream) -> 405 | {NewStream, Resource} = stream:take_and_pause(Stream), 406 | {RSrc, MaybeNewStream, SD} = Mod:on_offer(Resource, NewStream, StateData), 407 | {reply, {ok, RSrc}, ?PAUSED, MaybeNewStream#stream{mod_state = SD}}; 408 | 409 | paused({take, Number}, _From, #stream{is_closed = false, mod = undefined} = Stream) -> 410 | {NewStream, ResourceList} = stream:take(Stream, Number), 411 | {reply, {ok, ResourceList}, ?PAUSED, NewStream}; 412 | 413 | paused({take, Number}, _From, #stream{is_closed = false, mod = Mod, mod_state = StateData} = Stream) -> 414 | {NewStream, ResourceList} = stream:take(Stream, Number), 415 | {RSrcList, MaybeNewStream, SD} = Mod:on_offer(ResourceList, NewStream, StateData), 416 | {reply, {ok, RSrcList}, ?PAUSED, MaybeNewStream#stream{mod_state = SD}}; 417 | 418 | paused(_Event, _From, State) -> {reply, {error, bad_call}, ?PAUSED, State}. 419 | 420 | %% ========================================== 421 | %% DROPPING STATE 422 | %% ========================================== 423 | 424 | dropping({put, _Resource}, #stream{is_paused = true} = Stream) -> next_state(?PAUSED, Stream); 425 | dropping({put, _Resource}, #stream{is_stoped = true} = Stream) -> next_state(?STOPPED, Stream); 426 | dropping({put, _Resource}, #stream{is_closed = true} = Stream) -> next_state(?CLOSED, Stream); 427 | 428 | dropping({put, _Resource}, #stream{is_dropping = false} = Stream) -> 429 | open({put, _Resource}, stream:resume(Stream)); 430 | 431 | dropping({put, Resource}, #stream{} = Stream) when is_list(Resource) -> 432 | case stream:put_from_list(Stream, Resource) of 433 | {ok, #stream{is_dropping = false} = NewStream} -> {next_state, ?OPEN, NewStream}; 434 | {ok, #stream{is_dropping = true} = NewStream} -> {next_state, ?DROPPING, NewStream} 435 | end; 436 | 437 | dropping({put, Fn}, #stream{} = Stream) when is_function(Fn) -> 438 | case stream:put_while(Stream, Fn) of 439 | {ok, #stream{is_dropping = false} = NewStream} -> {next_state, ?OPEN, NewStream}; 440 | {ok, #stream{is_dropping = true} = NewStream} -> {next_state, ?DROPPING, NewStream} 441 | end; 442 | 443 | dropping({put, Resource}, #stream{mod = undefined} = Stream) -> 444 | case stream:put(Stream, Resource) of 445 | {ok, #stream{is_dropping = false} = NewStream} -> {next_state, ?OPEN, NewStream}; 446 | {ok, #stream{is_dropping = true} = NewStream} -> {next_state, ?DROPPING, NewStream}; 447 | {pause, NewStream} -> next_state(?PAUSED, NewStream); 448 | {stopped, NewStream} -> next_state(?STOPPED, NewStream); 449 | {closed, NewStream} -> next_state(?CLOSED, NewStream) 450 | end; 451 | 452 | dropping({put, Resource}, #stream{mod = Mod, mod_state = StateData} = Stream) -> 453 | case Mod:on_data(Resource, Stream, StateData) of 454 | {ignore, MaybeNewStream, SD} -> {next_state, ?OPEN, MaybeNewStream#stream{mod_state = SD}}; 455 | {ok, MaybeNewStream, SD} -> 456 | case stream:put(MaybeNewStream#stream{mod_state = SD}, Resource) of 457 | {ok, #stream{is_dropping = false} = NewStream} -> {next_state, ?OPEN, NewStream}; 458 | {ok, #stream{is_dropping = true} = NewStream} -> {next_state, ?DROPPING, NewStream}; 459 | {pause, NewStream} -> next_state(?PAUSED, NewStream); 460 | {stopped, NewStream} -> next_state(?STOPPED, NewStream); 461 | {closed, NewStream} -> next_state(?CLOSED, NewStream) 462 | end 463 | end; 464 | 465 | dropping(_Event, #stream{} = State) -> {next_state, ?OPEN, State}. 466 | 467 | %% ===== Syncronous ===== 468 | 469 | dropping(_Event, _From, #stream{is_closed = true} = Stream) -> reply({error, closed}, ?CLOSED, Stream); 470 | dropping(_Event, _From, #stream{is_stoped = true} = Stream) -> reply({error, stopped}, ?STOPPED, Stream); 471 | 472 | dropping(take, From, #stream{is_dropping = false} = Stream) -> 473 | open(take, From, stream:resume(Stream)); 474 | 475 | dropping(take, _From, #stream{is_closed = false, mod = undefined} = Stream) -> 476 | {NewStream, Resource} = stream:take(Stream), 477 | {reply, {ok, Resource}, ?DROPPING, NewStream}; 478 | 479 | dropping(take, _From, #stream{is_closed = false, mod = Mod, mod_state = StateData} = Stream) -> 480 | {NewStream, Resource} = stream:take(Stream), 481 | {RSrc, MaybeNewStream, SD} = Mod:on_offer(Resource, NewStream, StateData), 482 | {reply, {ok, RSrc}, ?DROPPING, MaybeNewStream#stream{mod_state = SD}}; 483 | 484 | dropping(take_and_pause, _From, #stream{is_closed = false, mod = undefined} = Stream) -> 485 | {NewStream, Resource} = stream:take_and_pause(Stream), 486 | {reply, {ok, Resource}, ?DROPPING, NewStream}; 487 | 488 | dropping(take_and_pause, _From, #stream{is_closed = false, mod = Mod, mod_state = StateData} = Stream) -> 489 | {NewStream, Resource} = stream:take_and_pause(Stream), 490 | {RSrc, MaybeNewStream, SD} = Mod:on_offer(Resource, NewStream, StateData), 491 | {reply, {ok, RSrc}, ?DROPPING, MaybeNewStream#stream{mod_state = SD}}; 492 | 493 | dropping({take, Number}, _From, #stream{is_closed = false, mod = undefined} = Stream) -> 494 | {NewStream, ResourceList} = stream:take(Stream, Number), 495 | {reply, {ok, ResourceList}, ?DROPPING, NewStream}; 496 | 497 | dropping({take, Number}, _From, #stream{is_closed = false, mod = Mod, mod_state = StateData} = Stream) -> 498 | {NewStream, ResourceList} = stream:take(Stream, Number), 499 | {RSrcList, MaybeNewStream, SD} = Mod:on_offer(ResourceList, NewStream, StateData), 500 | {reply, {ok, RSrcList}, ?DROPPING, MaybeNewStream#stream{mod_state = SD}}; 501 | 502 | dropping(_Event, _From, State) -> {reply, {error, bad_call}, ?DROPPING, State}. 503 | 504 | %% ========================================== 505 | %% STOPPED STATE 506 | %% ========================================== 507 | 508 | stopped(_Event, #stream{} = Stream) -> {next_state, ?STOPPED, Stream#stream{is_stoped = true}}. 509 | 510 | stopped(_Event, _From, #stream{} = Stream) -> {next_state, ?STOPPED, Stream#stream{is_stoped = true}}. 511 | 512 | %% ========================================== 513 | %% CLOSED STATE 514 | %% ========================================== 515 | 516 | closed(_Event, #stream{} = Stream) -> {next_state, ?CLOSED, Stream#stream{is_closed = true}}. 517 | 518 | closed(_Event, _From, #stream{} = Stream) -> {next_state, ?CLOSED, Stream#stream{is_closed = true}}. 519 | 520 | %% ========================================== 521 | %% PIPE CALL 522 | %% ========================================== 523 | 524 | handle_event({pipe, NextStreamPID}, _StateName, #stream{ 525 | is_closed = false, 526 | is_stoped = false 527 | } = Stream) -> {next_state, ?OPEN, stream:pipe(Stream, NextStreamPID)}; 528 | 529 | %% ========================================== 530 | %% DRAIN CALL 531 | %% ========================================== 532 | 533 | handle_event(drain, _StateName, #stream{ 534 | is_closed = false, 535 | is_stoped = false 536 | } = Stream) -> 537 | next_state(?OPEN, Stream#stream{is_paused = false}); 538 | 539 | %% ========================================== 540 | %% RESUME CALL 541 | %% ========================================== 542 | 543 | handle_event(resume, _StateName, #stream{ 544 | is_closed = false, 545 | is_stoped = false 546 | } = Stream) -> 547 | next_state(?OPEN, stream:resume(Stream)); 548 | 549 | %% ========================================== 550 | %% DROP CALL 551 | %% ========================================== 552 | 553 | handle_event(drop, _StateName, #stream{ 554 | is_closed = false, 555 | is_stoped = false 556 | } = Stream) -> 557 | {next_state, ?DROPPING, stream:drop(Stream)}; 558 | 559 | %% ========================================== 560 | %% DROP_WHILE CALL 561 | %% ========================================== 562 | 563 | handle_event({drop_while, Fn}, _StateName, #stream{ 564 | is_closed = false, 565 | is_stoped = false 566 | } = Stream) -> 567 | {next_state, ?DROPPING, stream:drop_while(Stream, Fn)}; 568 | 569 | %% ========================================== 570 | %% PAUSE CALL 571 | %% ========================================== 572 | 573 | handle_event(pause, _StateName, #stream{ 574 | is_closed = false, 575 | is_stoped = false 576 | } = Stream) -> 577 | next_state(?PAUSED, Stream#stream{is_paused = true}); 578 | 579 | %% ========================================== 580 | %% FILTER CALL 581 | %% ========================================== 582 | 583 | handle_event({filter, Fn}, _StateName, #stream{ 584 | is_closed = false, 585 | is_stoped = false 586 | } = Stream) -> {next_state, ?OPEN, stream:filter(Stream, Fn)}; 587 | 588 | %% ========================================== 589 | %% MAP CALL 590 | %% ========================================== 591 | 592 | handle_event({map, Fn}, _StateName, #stream{ 593 | is_closed = false, 594 | is_stoped = false 595 | } = Stream) -> {next_state, ?OPEN, stream:map(Stream, Fn)}; 596 | 597 | %% ========================================== 598 | %% REDUCE CALL 599 | %% ========================================== 600 | 601 | handle_event({reduce, Fn}, _StateName, #stream{ 602 | is_closed = false, 603 | is_stoped = false 604 | } = Stream) -> {next_state, ?OPEN, stream:reduce(Stream, Fn)}; 605 | 606 | handle_event({reduce, Fn, Acc}, _StateName, #stream{ 607 | is_closed = false, 608 | is_stoped = false 609 | } = Stream) -> {next_state, ?OPEN, stream:reduce(Stream, Fn, Acc)}; 610 | 611 | %% ========================================== 612 | %% FALLBACK CALL 613 | %% ========================================== 614 | 615 | handle_event(_Event, StateName, State) -> {next_state, StateName, State}. 616 | 617 | %% ========================================== 618 | %% ZIP CALL 619 | %% ========================================== 620 | 621 | %% TODO 622 | 623 | %% ========================================== 624 | %% IS_EMPTY CALL 625 | %% ========================================== 626 | 627 | handle_sync_event(is_empty, _From, StateName, #stream{} = Stream) -> 628 | {reply, stream:is_empty(Stream), StateName, Stream}; 629 | 630 | %% ========================================== 631 | %% IS_PAUSED CALL 632 | %% ========================================== 633 | 634 | handle_sync_event(can_accept, _From, ?OPEN, #stream{ 635 | is_paused = false, 636 | is_closed = false, 637 | is_stoped = false, 638 | buffer = Buffer, 639 | max_buffer = MAX 640 | } = Stream) when length(Buffer) < MAX -> 641 | {reply, true, ?OPEN, Stream}; 642 | 643 | handle_sync_event(can_accept, _From, StateName, #stream{} = Stream) -> 644 | {reply, false, StateName, Stream}; 645 | 646 | %% ========================================== 647 | %% IS_PAUSED CALL 648 | %% ========================================== 649 | 650 | handle_sync_event(is_paused, _From, ?PAUSED, #stream{} = Stream) -> 651 | {reply, true, ?PAUSED, Stream}; 652 | 653 | handle_sync_event(is_paused, _From, _StateName, #stream{ 654 | is_paused = false, 655 | buffer = Buffer, 656 | max_buffer = MAX 657 | } = Stream) when length(Buffer) >= MAX -> 658 | {reply, true, ?PAUSED, Stream}; 659 | 660 | handle_sync_event(is_paused, _From, StateName, #stream{} = Stream) -> 661 | {reply, false, StateName, Stream}; 662 | 663 | %% ========================================== 664 | %% IS_CLOSED CALL 665 | %% ========================================== 666 | 667 | handle_sync_event(is_closed, _From, ?CLOSED, #stream{} = Stream) -> 668 | {reply, true, ?CLOSED, Stream}; 669 | 670 | handle_sync_event(is_closed, _From, StateName, #stream{} = Stream) -> 671 | {reply, false, StateName, Stream}; 672 | 673 | %% ========================================== 674 | %% IS_STOPPED CALL 675 | %% ========================================== 676 | 677 | handle_sync_event(is_stopped, _From, ?STOPPED, #stream{} = Stream) -> 678 | {reply, true, ?STOPPED, Stream}; 679 | 680 | handle_sync_event(is_stopped, _From, StateName, #stream{} = Stream) -> 681 | {reply, false, StateName, Stream}; 682 | 683 | %% ========================================== 684 | %% IS_OPEN CALL 685 | %% ========================================== 686 | 687 | handle_sync_event(is_open, _From, ?OPEN, #stream{} = Stream) -> 688 | {reply, true, ?OPEN, Stream}; 689 | 690 | handle_sync_event(is_open, _From, StateName, #stream{} = Stream) -> 691 | {reply, false, StateName, Stream}; 692 | 693 | %% ========================================== 694 | %% GET_STREAM CALL 695 | %% ========================================== 696 | 697 | handle_sync_event(get_stream, _From, StateName, #stream{} = Stream) -> {reply, Stream, StateName, Stream}; 698 | 699 | %% ========================================== 700 | %% FALLBACK CALL 701 | %% ========================================== 702 | 703 | handle_sync_event(_Event, _From, StateName, State) -> 704 | {reply, {error, bad_call}, StateName, State}. 705 | 706 | handle_info(_Info, StateName, State) -> {next_state, StateName, State}. 707 | 708 | terminate(_Reason, _StateName, _State) -> ok. 709 | 710 | code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. 711 | 712 | %%%=================================================================== 713 | %%% Internal functions 714 | %%%=================================================================== 715 | 716 | next_state(State, #stream{mod = undefined} = Stream) -> 717 | {next_state, State, Stream}; 718 | 719 | next_state(State, #stream{mod = Mod, mod_state = StateData} = Stream) -> 720 | {ok, MaybeNewStateData} = Mod:on_state(State, Stream, StateData), 721 | {next_state, State, Stream#stream{mod_state = MaybeNewStateData}}. 722 | 723 | reply(Reply, State, #stream{mod = undefined} = Stream) -> 724 | {reply, Reply, State, Stream}; 725 | 726 | reply(Reply, State, #stream{mod = Mod, mod_state = StateData} = Stream) -> 727 | {ok, MaybeNewStateData} = Mod:on_state(State, Stream, StateData), 728 | {reply, Reply, State, Stream#stream{mod_state = MaybeNewStateData}}. 729 | -------------------------------------------------------------------------------- /src/stream.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% @author Evangelos Pappas 4 | %%% @copyright (C) 2015, evalonlabs 5 | %%% The MIT License (MIT) 6 | %%% 7 | %%% Copyright (c) 2015 Evangelos Pappas 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in all 17 | %%% copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | %%% SOFTWARE. 26 | %%% @doc 27 | %%% 28 | %%% @end 29 | %%%------------------------------------------------------------------- 30 | 31 | -module(stream). 32 | -author("epappas"). 33 | 34 | -include("erl_streams_commons.hrl"). 35 | 36 | %% API 37 | -export([ 38 | new/0, 39 | new/1, 40 | new/2, 41 | from_list/1, 42 | pause/1, 43 | drain/1, 44 | pipe/2, 45 | put/2, 46 | put_with_delay/3, 47 | put_from_list/2, 48 | put_while/2, 49 | take/1, 50 | take/2, 51 | take_with_delay/2, 52 | take_and_pause/1, 53 | resume/1, 54 | drop/1, 55 | drop_while/2, 56 | filter/2, 57 | map/2, 58 | reduce/2, 59 | reduce/3, 60 | zip/2, 61 | is_empty/1, 62 | is_dropping/1 63 | ]). 64 | 65 | -spec(new() -> #stream{}). 66 | new() -> new(new_stream). 67 | 68 | -spec(new(Name :: term()) -> #stream{}). 69 | new(Name) -> 70 | #stream{name = Name}. 71 | 72 | -spec(new(Name :: term(), Max :: number()) -> #stream{}). 73 | new(Name, Max) -> 74 | #stream{name = Name, max_buffer = Max}. 75 | 76 | -spec(from_list(List :: iolist()) -> #stream{}). 77 | from_list(List) -> 78 | Stream = new(from_list), 79 | {ok, NewStream} = stream:put_from_list(Stream, List), 80 | NewStream. 81 | 82 | -spec(pause(Stream :: #stream{}) -> #stream{}). 83 | pause(Stream) -> Stream#stream{is_paused = true}. 84 | 85 | -spec(drain(Stream :: #stream{}) -> #stream{}). 86 | drain(_Stream) -> 87 | #stream{is_paused = false}. 88 | 89 | -spec(put(Stream :: #stream{}, Resource :: any()) -> 90 | {ok, #stream{}} | {paused, #stream{} | {stopped, #stream{}} | {closed, #stream{}}}). 91 | put(#stream{is_paused = true} = Stream, _Resource) -> 92 | {pause, Stream}; 93 | put(#stream{is_stoped = true} = Stream, _Resource) -> 94 | {stopped, Stream}; 95 | put(#stream{is_closed = true} = Stream, _Resource) -> 96 | {closed, Stream}; 97 | put(#stream{buffer = Buffer, max_buffer = MAX} = Stream, _Resource) when length(Buffer) >= MAX -> 98 | %% a Guard to avoid back pressure 99 | {pause, Stream#stream{is_paused = true}}; 100 | 101 | put(#stream{ 102 | is_dropping = true, 103 | dropping_ctr = DR_Ctr, 104 | dropping_fn = Dropping_Fn 105 | } = Stream, Resource) -> 106 | case Dropping_Fn(Stream, Resource) of 107 | %% if codition no longer exists, retry 108 | false -> stream:put(resume(Stream), Resource); 109 | %% stream should keep dropping messages, just raise the counter 110 | true -> {ok, Stream#stream{ 111 | dropping_ctr = DR_Ctr + 1 112 | }} 113 | end; 114 | 115 | put(#stream{ 116 | is_paused = false, 117 | is_stoped = false, 118 | is_closed = false, 119 | is_dropping = false 120 | } = Stream, Resource) -> 121 | {ok, pre_waterfall_tick(Stream, Resource)}. 122 | 123 | -spec(put_with_delay(Stream :: #stream{}, Resource :: any(), Delay :: number()) -> 124 | {'ok', {integer(), reference()}} | {'error', term()}). 125 | put_with_delay(#stream{} = Stream, Resource, Delay) when Delay =:= 0 -> 126 | timer:apply_after(Delay, stream, put, [Stream, Resource]). 127 | 128 | -spec(put_from_list(Stream :: #stream{}, ResourceList :: list()) -> 129 | {ok, #stream{}} | {paused, #stream{} | {stopped, #stream{}} | {closed, #stream{}}}). 130 | put_from_list(#stream{} = Stream, ResourceList) when ResourceList =:= [] -> {ok, Stream#stream{}}; 131 | put_from_list(#stream{} = Stream, ResourceList) -> 132 | [H | T] = ResourceList, 133 | case stream:put(Stream, H) of 134 | {ok, NewStream} -> stream:put_from_list(NewStream, T); 135 | OtherState -> OtherState %% closed, isPaused, or anything else 136 | end. 137 | 138 | -spec(put_while(Stream :: #stream{}, Fn :: any()) -> 139 | {ok, #stream{}} | {paused, #stream{} | {stopped, #stream{}} | {closed, #stream{}}}). 140 | put_while(#stream{is_paused = false} = Stream, _Fn) -> 141 | {pause, Stream}; 142 | put_while(#stream{is_stoped = false} = Stream, _Fn) -> 143 | {stopped, Stream}; 144 | put_while(#stream{is_closed = true} = Stream, _Fn) -> 145 | {closed, Stream}; 146 | 147 | put_while(#stream{ 148 | is_paused = false, 149 | is_stoped = false, 150 | is_closed = false 151 | } = Stream, Fn) when is_function(Fn) -> 152 | case Fn(Stream) of 153 | undefined -> {ok, Stream}; 154 | Resource -> stream:put(Stream#stream{}, Resource) 155 | end. 156 | 157 | -spec(pipe(Stream :: #stream{}, pid()) -> #stream{}). 158 | pipe(#stream{pipes = Pipes} = Stream, NextStreamPID) when is_pid(NextStreamPID) -> 159 | Stream#stream{ 160 | pipes = lists:append( 161 | Pipes, [NextStreamPID] 162 | ) 163 | }. 164 | 165 | -spec(take(Stream :: #stream{}, Number :: number()) -> {#stream{}, list()}). 166 | take(#stream{is_closed = true} = Stream, _Number) -> {Stream, []}; 167 | take(#stream{} = Stream, Number) when Number =< 0 -> {Stream, []}; 168 | take(#stream{} = Stream, Number) -> take_loop(Stream, Number, []). 169 | 170 | take_loop(#stream{} = Stream, 0, SoFar) -> {Stream, SoFar}; 171 | take_loop(#stream{buffer = Buffer} = Stream, _Number, SoFar) when Buffer =:= [] -> {Stream, SoFar}; 172 | take_loop(#stream{buffer = Buffer} = Stream, Number, SoFar) -> 173 | [H | T] = Buffer, 174 | take_loop(Stream#stream{buffer = T}, Number - 1, lists:append(SoFar, H)). 175 | 176 | -spec(take(Stream :: #stream{}) -> {#stream{}, iolist()}). 177 | take(#stream{buffer = Buffer} = Stream) when Buffer =:= [] -> {Stream#stream{}, undefined}; 178 | take(#stream{buffer = Buffer} = Stream) -> 179 | [Value | RestValues] = Buffer, 180 | {Stream#stream{buffer = RestValues}, Value}. 181 | 182 | -spec(take_with_delay(Stream :: #stream{}, Delay :: number()) -> {#stream{}, iolist()}). 183 | take_with_delay(#stream{} = Stream, Delay) -> 184 | ok = timer:sleep(Delay), 185 | take(Stream). 186 | 187 | -spec(take_and_pause(Stream :: #stream{}) -> {#stream{}, iolist()}). 188 | take_and_pause(#stream{} = Stream) -> 189 | {NewStream, Resource} = take(Stream), 190 | {NewStream#stream{is_paused = true}, Resource}. 191 | 192 | -spec(resume(Stream :: #stream{}) -> #stream{}). 193 | resume(#stream{} = Stream) -> 194 | Stream#stream{ 195 | is_dropping = false, 196 | dropping_ctr = 0, 197 | dropping_fn = undefined 198 | }. 199 | 200 | -spec(drop(Stream :: #stream{}) -> #stream{}). 201 | drop(#stream{} = Stream) -> 202 | Stream#stream{ 203 | is_dropping = true, 204 | dropping_ctr = 0, 205 | dropping_fn = 206 | fun(#stream{ 207 | dropping_ctr = DR_Ctr 208 | } = _ThisStream, _Resource) -> 209 | DR_Ctr < 1 210 | end 211 | }. 212 | 213 | -spec(drop_while(Stream :: #stream{}, Fn :: any()) -> #stream{}). 214 | drop_while(Stream, Dropping_Cond_FN) -> 215 | Stream#stream{ 216 | is_dropping = true, 217 | dropping_ctr = 0, 218 | dropping_fn = Dropping_Cond_FN 219 | }. 220 | 221 | -spec(filter(Stream :: #stream{}, Fn :: any()) -> #stream{}). 222 | filter(#stream{pre_waterfall = PreWaterfall} = Stream, Fn) when is_function(Fn) -> 223 | Stream#stream{ 224 | pre_waterfall = lists:append( 225 | PreWaterfall, [[{type, filter}, {fn, Fn}]] 226 | ) 227 | }. 228 | 229 | -spec(map(Stream :: #stream{}, Fn :: any()) -> #stream{}). 230 | map(#stream{pre_waterfall = PreWaterfall} = Stream, Fn) when is_function(Fn) -> 231 | Stream#stream{ 232 | pre_waterfall = lists:append( 233 | PreWaterfall, [[{type, map}, {fn, Fn}]] 234 | ) 235 | }. 236 | 237 | -spec(reduce(Stream :: #stream{}, Fn :: any()) -> #stream{}). 238 | reduce(Stream, Fn) -> reduce(Stream, Fn, undefined). 239 | 240 | -spec(reduce(Stream :: #stream{}, Fn :: any(), Acc :: any()) -> #stream{}). 241 | reduce(#stream{pre_waterfall = PreWaterfall} = Stream, Fn, Acc) when is_function(Fn) -> 242 | Stream#stream{ 243 | reduce_acc = Acc, 244 | pre_waterfall = lists:append( 245 | PreWaterfall, [[ 246 | {type, reduce}, 247 | {fn, Fn} 248 | ]] 249 | ) 250 | }. 251 | 252 | -spec(zip(Left :: #stream{}, Right :: #stream{}) -> #stream{}). 253 | zip(#stream{buffer = LBuffer, pre_waterfall = LPRW, post_waterfall = LPOW} = _Left, 254 | #stream{buffer = RBuffer, pre_waterfall = RPRW, post_waterfall = RPOW} = _Right) -> 255 | Stream = new(), 256 | Stream#stream{ 257 | buffer = lists:append(LBuffer, RBuffer), 258 | pre_waterfall = lists:append(LPRW, RPRW), 259 | post_waterfall = lists:append(LPOW, RPOW) 260 | } 261 | . 262 | 263 | -spec(is_empty(Stream :: #stream{}) -> boolean()). 264 | is_empty(#stream{buffer = []} = _Stream) -> true; 265 | is_empty(#stream{} = _Stream) -> false. 266 | 267 | -spec(is_dropping(Stream :: #stream{}) -> boolean()). 268 | is_dropping(#stream{is_dropping = true} = _Stream) -> true; 269 | is_dropping(#stream{} = _Stream) -> false. 270 | 271 | %%%=================================================================== 272 | %%% Internal functions 273 | %%%=================================================================== 274 | 275 | pre_waterfall_tick(#stream{pre_waterfall = PREW} = Stream, Resource) -> 276 | pre_waterfall_tick(Stream, Resource, PREW). 277 | 278 | pre_waterfall_tick(#stream{} = Stream, undefined, _PREW) -> 279 | Stream; 280 | 281 | pre_waterfall_tick(#stream{buffer = Buffer} = Stream, Resource, []) -> 282 | Stream#stream{ 283 | buffer = lists:append(Buffer, [Resource]) 284 | }; 285 | 286 | pre_waterfall_tick(#stream{} = Stream, Resource, [Next | RestPREW]) -> 287 | 288 | [Type | RestArgs] = Next, 289 | 290 | {NewStream, NewResource} = 291 | case Type of 292 | {type, map} -> 293 | pre_waterfall_map(Stream, Resource, RestArgs); 294 | {type, filter} -> 295 | pre_waterfall_filter(Stream, Resource, RestArgs); 296 | {type, reduce} -> 297 | pre_waterfall_reduce(Stream, Resource, RestArgs) 298 | end, 299 | 300 | pre_waterfall_tick(NewStream, NewResource, RestPREW). 301 | 302 | pre_waterfall_map(#stream{buffer = Buffer} = Stream, Resource, [{fn, MapFn}]) -> 303 | {Stream, MapFn(Resource, Buffer)}. 304 | 305 | pre_waterfall_filter(#stream{buffer = Buffer} = Stream, Resource, [{fn, FilterFn}]) -> 306 | NewResource = 307 | case FilterFn(Resource, Buffer) of 308 | true -> Resource; 309 | _ -> undefined 310 | end, 311 | {Stream#stream{}, NewResource}. 312 | 313 | pre_waterfall_reduce(#stream{ 314 | reduce_acc = Acc, 315 | buffer = Buffer 316 | } = Stream, Resource, [{fn, ReduceFn}]) -> 317 | 318 | NewAcc = ReduceFn(Acc, Resource, Buffer), 319 | 320 | {Stream#stream{reduce_acc = NewAcc, buffer = []}, NewAcc}. 321 | 322 | -------------------------------------------------------------------------------- /tests/add2_stream.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% @author Evangelos Pappas 4 | %%% @copyright (C) 2015, evalonlabs 5 | %%% The MIT License (MIT) 6 | %%% 7 | %%% Copyright (c) 2015 Evangelos Pappas 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in all 17 | %%% copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | %%% SOFTWARE. 26 | %%% @doc 27 | %%% 28 | %%% @end 29 | %%%------------------------------------------------------------------- 30 | 31 | -module(add2_stream). 32 | -author("epappas"). 33 | 34 | -behaviour(gen_stream). 35 | 36 | %% API 37 | 38 | %% gen_stream callbacks 39 | -export([init/1, on_data/3, on_offer/3, on_state/3]). 40 | 41 | -define(SERVER, ?MODULE). 42 | 43 | %%%=================================================================== 44 | %%% API 45 | %%%=================================================================== 46 | 47 | init(_Args) -> {ok, {}}. 48 | 49 | on_data(_Resource, Stream, State) -> 50 | {ok, Stream, State}. 51 | 52 | on_offer(Resource, Stream, State) -> 53 | {Resource + 2, Stream, State}. 54 | 55 | on_state(State, _Stream, StateData) -> 56 | {ok, StateData}. 57 | 58 | %%%=================================================================== 59 | %%% Internal functions 60 | %%%=================================================================== 61 | -------------------------------------------------------------------------------- /tests/etap.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2008-2009 Nick Gerakines 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person 4 | %% obtaining a copy of this software and associated documentation 5 | %% files (the "Software"), to deal in the Software without 6 | %% restriction, including without limitation the rights to use, 7 | %% copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | %% copies of the Software, and to permit persons to whom the 9 | %% Software is furnished to do so, subject to the following 10 | %% conditions: 11 | %% 12 | %% The above copyright notice and this permission notice shall be 13 | %% included in all copies or substantial portions of the Software. 14 | %% 15 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | %% OTHER DEALINGS IN THE SOFTWARE. 23 | %% 24 | %% @author Nick Gerakines [http://socklabs.com/] 25 | %% @author Jeremy Wall 26 | %% @version 0.3.4 27 | %% @copyright 2007-2008 Jeremy Wall, 2008-2009 Nick Gerakines 28 | %% @reference http://testanything.org/wiki/index.php/Main_Page 29 | %% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol 30 | %% @todo Finish implementing the skip directive. 31 | %% @todo Document the messages handled by this receive loop. 32 | %% @todo Explain in documentation why we use a process to handle test input. 33 | %% @doc etap is a TAP testing module for Erlang components and applications. 34 | %% This module allows developers to test their software using the TAP method. 35 | %% 36 | %%

37 | %% TAP, the Test Anything Protocol, is a simple text-based interface between 38 | %% testing modules in a test harness. TAP started life as part of the test 39 | %% harness for Perl but now has implementations in C/C++, Python, PHP, Perl 40 | %% and probably others by the time you read this. 41 | %%

42 | %% 43 | %% The testing process begins by defining a plan using etap:plan/1, running 44 | %% a number of etap tests and then calling eta:end_tests/0. Please refer to 45 | %% the Erlang modules in the t directory of this project for example tests. 46 | -module(etap). 47 | -vsn("0.3.4"). 48 | 49 | -export([ 50 | ensure_test_server/0, 51 | start_etap_server/0, 52 | test_server/1, 53 | msg/1, msg/2, 54 | diag/1, diag/2, 55 | expectation_mismatch_message/3, 56 | plan/1, 57 | end_tests/0, 58 | not_ok/2, ok/2, is_ok/2, is/3, isnt/3, any/3, none/3, 59 | fun_is/3, expect_fun/3, expect_fun/4, 60 | is_greater/3, 61 | skip/1, skip/2, 62 | datetime/1, 63 | skip/3, 64 | bail/0, bail/1, 65 | test_state/0, failure_count/0 66 | ]). 67 | 68 | -export([ 69 | contains_ok/3, 70 | is_before/4 71 | ]). 72 | 73 | -export([ 74 | is_pid/2, 75 | is_alive/2, 76 | is_mfa/3 77 | ]). 78 | 79 | -export([ 80 | loaded_ok/2, 81 | can_ok/2, can_ok/3, 82 | has_attrib/2, is_attrib/3, 83 | is_behaviour/2 84 | ]). 85 | 86 | -export([ 87 | dies_ok/2, 88 | lives_ok/2, 89 | throws_ok/3 90 | ]). 91 | 92 | 93 | -record(test_state, { 94 | planned = 0, 95 | count = 0, 96 | pass = 0, 97 | fail = 0, 98 | skip = 0, 99 | skip_reason = "" 100 | }). 101 | 102 | %% @spec plan(N) -> Result 103 | %% N = unknown | skip | {skip, string()} | integer() 104 | %% Result = ok 105 | %% @doc Create a test plan and boot strap the test server. 106 | plan(unknown) -> 107 | ensure_test_server(), 108 | etap_server ! {self(), plan, unknown}, 109 | ok; 110 | plan(skip) -> 111 | io:format("1..0 # skip~n"); 112 | plan({skip, Reason}) -> 113 | io:format("1..0 # skip ~s~n", [Reason]); 114 | plan(N) when is_integer(N), N > 0 -> 115 | ensure_test_server(), 116 | etap_server ! {self(), plan, N}, 117 | ok. 118 | 119 | %% @spec end_tests() -> ok 120 | %% @doc End the current test plan and output test results. 121 | %% @todo This should probably be done in the test_server process. 122 | end_tests() -> 123 | case whereis(etap_server) of 124 | undefined -> self() ! true; 125 | _ -> etap_server ! {self(), state} 126 | end, 127 | State = receive X -> X end, 128 | if 129 | State#test_state.planned == -1 -> 130 | io:format("1..~p~n", [State#test_state.count]); 131 | true -> 132 | ok 133 | end, 134 | case whereis(etap_server) of 135 | undefined -> ok; 136 | _ -> etap_server ! done, ok 137 | end. 138 | 139 | bail() -> 140 | bail(""). 141 | 142 | bail(Reason) -> 143 | etap_server ! {self(), diag, "Bail out! " ++ Reason}, 144 | etap_server ! done, ok, 145 | ok. 146 | 147 | %% @spec test_state() -> Return 148 | %% Return = test_state_record() | {error, string()} 149 | %% @doc Return the current test state 150 | test_state() -> 151 | etap_server ! {self(), state}, 152 | receive 153 | X when is_record(X, test_state) -> X 154 | after 155 | 1000 -> {error, "Timed out waiting for etap server reply.~n"} 156 | end. 157 | 158 | %% @spec failure_count() -> Return 159 | %% Return = integer() | {error, string()} 160 | %% @doc Return the current failure count 161 | failure_count() -> 162 | case test_state() of 163 | #test_state{fail = FailureCount} -> FailureCount; 164 | X -> X 165 | end. 166 | 167 | %% @spec msg(S) -> ok 168 | %% S = string() 169 | %% @doc Print a message in the test output. 170 | msg(S) -> etap_server ! {self(), diag, S}, ok. 171 | 172 | %% @spec msg(Format, Data) -> ok 173 | %% Format = atom() | string() | binary() 174 | %% Data = [term()] 175 | %% UnicodeList = [Unicode] 176 | %% Unicode = int() 177 | %% @doc Print a message in the test output. 178 | %% Function arguments are passed through io_lib:format/2. 179 | msg(Format, Data) -> msg(io_lib:format(Format, Data)). 180 | 181 | %% @spec diag(S) -> ok 182 | %% S = string() 183 | %% @doc Print a debug/status message related to the test suite. 184 | diag(S) -> msg("# " ++ S). 185 | 186 | %% @spec diag(Format, Data) -> ok 187 | %% Format = atom() | string() | binary() 188 | %% Data = [term()] 189 | %% UnicodeList = [Unicode] 190 | %% Unicode = int() 191 | %% @doc Print a debug/status message related to the test suite. 192 | %% Function arguments are passed through io_lib:format/2. 193 | diag(Format, Data) -> diag(io_lib:format(Format, Data)). 194 | 195 | %% @spec expectation_mismatch_message(Got, Expected, Desc) -> ok 196 | %% Got = any() 197 | %% Expected = any() 198 | %% Desc = string() 199 | %% @doc Print an expectation mismatch message in the test output. 200 | expectation_mismatch_message(Got, Expected, Desc) -> 201 | msg(" ---"), 202 | msg(" description: ~p", [Desc]), 203 | msg(" found: ~p", [Got]), 204 | msg(" wanted: ~p", [Expected]), 205 | msg(" ..."), 206 | ok. 207 | 208 | % @spec evaluate(Pass, Got, Expected, Desc) -> Result 209 | %% Pass = true | false 210 | %% Got = any() 211 | %% Expected = any() 212 | %% Desc = string() 213 | %% Result = true | false 214 | %% @doc Evaluate a test statement, printing an expectation mismatch message 215 | %% if the test failed. 216 | evaluate(Pass, Got, Expected, Desc) -> 217 | case mk_tap(Pass, Desc) of 218 | false -> 219 | expectation_mismatch_message(Got, Expected, Desc), 220 | false; 221 | true -> 222 | true 223 | end. 224 | 225 | %% @spec ok(Expr, Desc) -> Result 226 | %% Expr = true | false 227 | %% Desc = string() 228 | %% Result = true | false 229 | %% @doc Assert that a statement is true. 230 | ok(Expr, Desc) -> evaluate(Expr == true, Expr, true, Desc). 231 | 232 | %% @spec not_ok(Expr, Desc) -> Result 233 | %% Expr = true | false 234 | %% Desc = string() 235 | %% Result = true | false 236 | %% @doc Assert that a statement is false. 237 | not_ok(Expr, Desc) -> evaluate(Expr == false, Expr, false, Desc). 238 | 239 | %% @spec is_ok(Expr, Desc) -> Result 240 | %% Expr = any() 241 | %% Desc = string() 242 | %% Result = true | false 243 | %% @doc Assert that two values are the same. 244 | is_ok(Expr, Desc) -> evaluate(Expr == ok, Expr, ok, Desc). 245 | 246 | %% @spec is(Got, Expected, Desc) -> Result 247 | %% Got = any() 248 | %% Expected = any() 249 | %% Desc = string() 250 | %% Result = true | false 251 | %% @doc Assert that two values are the same. 252 | is(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc). 253 | 254 | %% @spec isnt(Got, Expected, Desc) -> Result 255 | %% Got = any() 256 | %% Expected = any() 257 | %% Desc = string() 258 | %% Result = true | false 259 | %% @doc Assert that two values are not the same. 260 | isnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc). 261 | 262 | %% @spec is_greater(ValueA, ValueB, Desc) -> Result 263 | %% ValueA = number() 264 | %% ValueB = number() 265 | %% Desc = string() 266 | %% Result = true | false 267 | %% @doc Assert that an integer is greater than another. 268 | is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) -> 269 | mk_tap(ValueA > ValueB, Desc). 270 | 271 | %% @spec any(Got, Items, Desc) -> Result 272 | %% Got = any() 273 | %% Items = [any()] 274 | %% Desc = string() 275 | %% Result = true | false 276 | %% @doc Assert that an item is in a list. 277 | any(Got, Items, Desc) when is_function(Got) -> 278 | is(lists:any(Got, Items), true, Desc); 279 | any(Got, Items, Desc) -> 280 | is(lists:member(Got, Items), true, Desc). 281 | 282 | %% @spec none(Got, Items, Desc) -> Result 283 | %% Got = any() 284 | %% Items = [any()] 285 | %% Desc = string() 286 | %% Result = true | false 287 | %% @doc Assert that an item is not in a list. 288 | none(Got, Items, Desc) when is_function(Got) -> 289 | is(lists:any(Got, Items), false, Desc); 290 | none(Got, Items, Desc) -> 291 | is(lists:member(Got, Items), false, Desc). 292 | 293 | %% @spec fun_is(Fun, Expected, Desc) -> Result 294 | %% Fun = function() 295 | %% Expected = any() 296 | %% Desc = string() 297 | %% Result = true | false 298 | %% @doc Use an anonymous function to assert a pattern match. 299 | fun_is(Fun, Expected, Desc) when is_function(Fun) -> 300 | is(Fun(Expected), true, Desc). 301 | 302 | %% @spec expect_fun(ExpectFun, Got, Desc) -> Result 303 | %% ExpectFun = function() 304 | %% Got = any() 305 | %% Desc = string() 306 | %% Result = true | false 307 | %% @doc Use an anonymous function to assert a pattern match, using actual 308 | %% value as the argument to the function. 309 | expect_fun(ExpectFun, Got, Desc) -> 310 | evaluate(ExpectFun(Got), Got, ExpectFun, Desc). 311 | 312 | %% @spec expect_fun(ExpectFun, Got, Desc, ExpectStr) -> Result 313 | %% ExpectFun = function() 314 | %% Got = any() 315 | %% Desc = string() 316 | %% ExpectStr = string() 317 | %% Result = true | false 318 | %% @doc Use an anonymous function to assert a pattern match, using actual 319 | %% value as the argument to the function. 320 | expect_fun(ExpectFun, Got, Desc, ExpectStr) -> 321 | evaluate(ExpectFun(Got), Got, ExpectStr, Desc). 322 | 323 | %% @equiv skip(TestFun, "") 324 | skip(TestFun) when is_function(TestFun) -> 325 | skip(TestFun, ""). 326 | 327 | %% @spec skip(TestFun, Reason) -> ok 328 | %% TestFun = function() 329 | %% Reason = string() 330 | %% @doc Skip a test. 331 | skip(TestFun, Reason) when is_function(TestFun), is_list(Reason) -> 332 | begin_skip(Reason), 333 | catch TestFun(), 334 | end_skip(), 335 | ok. 336 | 337 | %% @spec skip(Q, TestFun, Reason) -> ok 338 | %% Q = true | false | function() 339 | %% TestFun = function() 340 | %% Reason = string() 341 | %% @doc Skips a test conditionally. The first argument to this function can 342 | %% either be the 'true' or 'false' atoms or a function that returns 'true' or 343 | %% 'false'. 344 | skip(QFun, TestFun, Reason) when is_function(QFun), is_function(TestFun), is_list(Reason) -> 345 | case QFun() of 346 | true -> begin_skip(Reason), TestFun(), end_skip(); 347 | _ -> TestFun() 348 | end, 349 | ok; 350 | 351 | skip(Q, TestFun, Reason) when is_function(TestFun), is_list(Reason), Q == true -> 352 | begin_skip(Reason), 353 | TestFun(), 354 | end_skip(), 355 | ok; 356 | 357 | skip(_, TestFun, Reason) when is_function(TestFun), is_list(Reason) -> 358 | TestFun(), 359 | ok. 360 | 361 | %% @private 362 | begin_skip(Reason) -> 363 | etap_server ! {self(), begin_skip, Reason}. 364 | 365 | %% @private 366 | end_skip() -> 367 | etap_server ! {self(), end_skip}. 368 | 369 | %% @spec contains_ok(string(), string(), string()) -> true | false 370 | %% @doc Assert that a string is contained in another string. 371 | contains_ok(Source, String, Desc) -> 372 | etap:isnt( 373 | string:str(Source, String), 374 | 0, 375 | Desc 376 | ). 377 | 378 | %% @spec is_before(string(), string(), string(), string()) -> true | false 379 | %% @doc Assert that a string comes before another string within a larger body. 380 | is_before(Source, StringA, StringB, Desc) -> 381 | etap:is_greater( 382 | string:str(Source, StringB), 383 | string:str(Source, StringA), 384 | Desc 385 | ). 386 | 387 | %% @doc Assert that a given variable is a pid. 388 | is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc); 389 | is_pid(_, Desc) -> etap:ok(false, Desc). 390 | 391 | %% @doc Assert that a given process/pid is alive. 392 | is_alive(Pid, Desc) -> 393 | etap:ok(erlang:is_process_alive(Pid), Desc). 394 | 395 | %% @doc Assert that the current function of a pid is a given {M, F, A} tuple. 396 | is_mfa(Pid, MFA, Desc) -> 397 | etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc). 398 | 399 | %% @spec loaded_ok(atom(), string()) -> true | false 400 | %% @doc Assert that a module has been loaded successfully. 401 | loaded_ok(M, Desc) when is_atom(M) -> 402 | etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc). 403 | 404 | %% @spec can_ok(atom(), atom()) -> true | false 405 | %% @doc Assert that a module exports a given function. 406 | can_ok(M, F) when is_atom(M), is_atom(F) -> 407 | Matches = [X || {X, _} <- M:module_info(exports), X == F], 408 | etap:ok(Matches > 0, lists:concat([M, " can ", F])). 409 | 410 | %% @spec can_ok(atom(), atom(), integer()) -> true | false 411 | %% @doc Assert that a module exports a given function with a given arity. 412 | can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) -> 413 | Matches = [X || X <- M:module_info(exports), X == {F, A}], 414 | etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])). 415 | 416 | %% @spec has_attrib(M, A) -> true | false 417 | %% M = atom() 418 | %% A = atom() 419 | %% @doc Asserts that a module has a given attribute. 420 | has_attrib(M, A) when is_atom(M), is_atom(A) -> 421 | etap:isnt( 422 | proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'), 423 | 'asdlkjasdlkads', 424 | lists:concat([M, " has attribute ", A]) 425 | ). 426 | 427 | %% @spec has_attrib(M, A. V) -> true | false 428 | %% M = atom() 429 | %% A = atom() 430 | %% V = any() 431 | %% @doc Asserts that a module has a given attribute with a given value. 432 | is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) -> 433 | etap:is( 434 | proplists:get_value(A, M:module_info(attributes)), 435 | [V], 436 | lists:concat([M, "'s ", A, " is ", V]) 437 | ). 438 | 439 | %% @spec is_behavior(M, B) -> true | false 440 | %% M = atom() 441 | %% B = atom() 442 | %% @doc Asserts that a given module has a specific behavior. 443 | is_behaviour(M, B) when is_atom(M) andalso is_atom(B) -> 444 | is_attrib(M, behaviour, B). 445 | 446 | %% @doc Assert that an exception is raised when running a given function. 447 | dies_ok(F, Desc) -> 448 | case (catch F()) of 449 | {'EXIT', _} -> etap:ok(true, Desc); 450 | _ -> etap:ok(false, Desc) 451 | end. 452 | 453 | %% @doc Assert that an exception is not raised when running a given function. 454 | lives_ok(F, Desc) -> 455 | etap:is(try_this(F), success, Desc). 456 | 457 | %% @doc Assert that the exception thrown by a function matches the given exception. 458 | throws_ok(F, Exception, Desc) -> 459 | try F() of 460 | _ -> etap:ok(nok, Desc) 461 | catch 462 | _:E -> 463 | etap:is(E, Exception, Desc) 464 | end. 465 | 466 | %% @private 467 | %% @doc Run a function and catch any exceptions. 468 | try_this(F) when is_function(F, 0) -> 469 | try F() of 470 | _ -> success 471 | catch 472 | throw:E -> {throw, E}; 473 | error:E -> {error, E}; 474 | exit:E -> {exit, E} 475 | end. 476 | 477 | %% @private 478 | %% @doc Start the etap_server process if it is not running already. 479 | ensure_test_server() -> 480 | case whereis(etap_server) of 481 | undefined -> 482 | proc_lib:start(?MODULE, start_etap_server, []); 483 | _ -> 484 | diag("The test server is already running.") 485 | end. 486 | 487 | %% @private 488 | %% @doc Start the etap_server loop and register itself as the etap_server 489 | %% process. 490 | start_etap_server() -> 491 | catch register(etap_server, self()), 492 | proc_lib:init_ack(ok), 493 | etap:test_server(#test_state{ 494 | planned = 0, 495 | count = 0, 496 | pass = 0, 497 | fail = 0, 498 | skip = 0, 499 | skip_reason = "" 500 | }). 501 | 502 | 503 | %% @private 504 | %% @doc The main etap_server receive/run loop. The etap_server receive loop 505 | %% responds to seven messages apperatining to failure or passing of tests. 506 | %% It is also used to initiate the testing process with the {_, plan, _} 507 | %% message that clears the current test state. 508 | test_server(State) -> 509 | NewState = receive 510 | {_From, plan, unknown} -> 511 | io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), 512 | io:format("# Using etap version ~p~n", [proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info()))]), 513 | State#test_state{ 514 | planned = -1, 515 | count = 0, 516 | pass = 0, 517 | fail = 0, 518 | skip = 0, 519 | skip_reason = "" 520 | }; 521 | {_From, plan, N} -> 522 | io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), 523 | io:format("# Using etap version ~p~n", [proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info()))]), 524 | io:format("1..~p~n", [N]), 525 | State#test_state{ 526 | planned = N, 527 | count = 0, 528 | pass = 0, 529 | fail = 0, 530 | skip = 0, 531 | skip_reason = "" 532 | }; 533 | {_From, begin_skip, Reason} -> 534 | State#test_state{ 535 | skip = 1, 536 | skip_reason = Reason 537 | }; 538 | {_From, end_skip} -> 539 | State#test_state{ 540 | skip = 0, 541 | skip_reason = "" 542 | }; 543 | {_From, pass, Desc} -> 544 | FullMessage = skip_diag( 545 | " - " ++ Desc, 546 | State#test_state.skip, 547 | State#test_state.skip_reason 548 | ), 549 | io:format("ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), 550 | State#test_state{ 551 | count = State#test_state.count + 1, 552 | pass = State#test_state.pass + 1 553 | }; 554 | 555 | {_From, fail, Desc} -> 556 | FullMessage = skip_diag( 557 | " - " ++ Desc, 558 | State#test_state.skip, 559 | State#test_state.skip_reason 560 | ), 561 | io:format("not ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), 562 | State#test_state{ 563 | count = State#test_state.count + 1, 564 | fail = State#test_state.fail + 1 565 | }; 566 | {From, state} -> 567 | From ! State, 568 | State; 569 | {_From, diag, Message} -> 570 | io:format("~s~n", [Message]), 571 | State; 572 | {From, count} -> 573 | From ! State#test_state.count, 574 | State; 575 | {From, is_skip} -> 576 | From ! State#test_state.skip, 577 | State; 578 | done -> 579 | exit(normal) 580 | end, 581 | test_server(NewState). 582 | 583 | %% @private 584 | %% @doc Process the result of a test and send it to the etap_server process. 585 | mk_tap(Result, Desc) -> 586 | IsSkip = lib:sendw(etap_server, is_skip), 587 | case [IsSkip, Result] of 588 | [_, true] -> 589 | etap_server ! {self(), pass, Desc}, 590 | true; 591 | [1, _] -> 592 | etap_server ! {self(), pass, Desc}, 593 | true; 594 | _ -> 595 | etap_server ! {self(), fail, Desc}, 596 | false 597 | end. 598 | 599 | %% @private 600 | %% @doc Format a date/time string. 601 | datetime(DateTime) -> 602 | {{Year, Month, Day}, {Hour, Min, Sec}} = DateTime, 603 | io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B ~2.10.0B:~2.10.0B:~2.10.0B", [Year, Month, Day, Hour, Min, Sec]). 604 | 605 | %% @private 606 | %% @doc Craft an output message taking skip/todo into consideration. 607 | skip_diag(Message, 0, _) -> 608 | Message; 609 | skip_diag(_Message, 1, "") -> 610 | " # SKIP"; 611 | skip_diag(_Message, 1, Reason) -> 612 | " # SKIP : " ++ Reason. -------------------------------------------------------------------------------- /tests/gen_stream.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%% -*- erlang -*- 3 | %%! -pa ./ebin -pa ./tests 4 | %%%------------------------------------------------------------------- 5 | %%% @author Evangelos Pappas 6 | %%% @copyright (C) 2015, evalonlabs 7 | %%% The MIT License (MIT) 8 | %%% 9 | %%% Copyright (c) 2015 Evangelos Pappas 10 | %%% 11 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 12 | %%% of this software and associated documentation files (the "Software"), to deal 13 | %%% in the Software without restriction, including without limitation the rights 14 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | %%% copies of the Software, and to permit persons to whom the Software is 16 | %%% furnished to do so, subject to the following conditions: 17 | %%% 18 | %%% The above copyright notice and this permission notice shall be included in all 19 | %%% copies or substantial portions of the Software. 20 | %%% 21 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | %%% SOFTWARE. 28 | %%% @doc 29 | %%% 30 | %%% @end 31 | %%%------------------------------------------------------------------- 32 | 33 | -include("../src/erl_streams_commons.hrl"). 34 | 35 | main(_) -> 36 | etap:plan(unknown), 37 | 38 | test_in_n_out(), 39 | test_pause(), 40 | test_max_buffer(), 41 | 42 | etap:end_tests(), 43 | ok. 44 | 45 | test_in_n_out() -> 46 | {ok, StreamPID} = gen_stream:start(test), 47 | 48 | #stream{} = gen_stream:get_stream(StreamPID), 49 | 50 | etap:is(gen_stream:is_open(StreamPID), true, "new gen_streamshould be open"), 51 | 52 | etap:is(gen_stream:is_empty(StreamPID), true, "new gen_streamshould be empty"), 53 | 54 | etap:is_ok(gen_stream:put(StreamPID, 1), "gen_stream:put should return ok"), 55 | 56 | etap:is(gen_stream:take(StreamPID), {ok, 1},"gen_stream:take should pop the buffer"), 57 | 58 | etap:is(gen_stream:take(StreamPID), {ok, undefined},"gen_stream:take should pop undefined"). 59 | 60 | test_pause() -> 61 | {ok, StreamPID} = gen_stream:start(test), 62 | 63 | etap:is_ok(gen_stream:pause(StreamPID), "gen_stream:pause should return ok"), 64 | 65 | etap:is(gen_stream:put(StreamPID, test), {error, pause}, "paused stream should return {error, pause} on put"), 66 | 67 | etap:is_ok(gen_stream:drain(StreamPID), "gen_stream:drain should return ok"), 68 | 69 | etap:is_ok(gen_stream:drain(StreamPID), "gen_stream:drain should return ok no matter what state"), 70 | 71 | etap:is_ok(gen_stream:put(StreamPID, 1), "gen_stream:put should return ok after drain"), 72 | 73 | etap:is(gen_stream:take_and_pause(StreamPID), {ok, 1}, "gen_stream:take_and_pause should pop the buffer"), 74 | 75 | etap:is(gen_stream:is_paused(StreamPID), true, "gen_stream:take_and_pause should pause the stream"). 76 | 77 | test_max_buffer() -> 78 | {ok, StreamPID} = gen_stream:start(test, 1), 79 | 80 | ok = gen_stream:put(StreamPID, test), 81 | 82 | etap:is(gen_stream:put(StreamPID, test), {error, pause}, "MAX buffer should have succesfully been reached"), 83 | 84 | ok = gen_stream:drain(StreamPID), 85 | 86 | etap:is(gen_stream:put(StreamPID, test), {error, pause}, "drain() shouldn't overite max_buffer state"), 87 | 88 | {ok, test} = gen_stream:take(StreamPID), 89 | {ok, undefined} = gen_stream:take(StreamPID), 90 | 91 | ok = gen_stream:drain(StreamPID), 92 | etap:is_ok(gen_stream:put(StreamPID, test), "When backpresure is resolved, should unpause after drain"), 93 | etap:is(gen_stream:take(StreamPID), {ok, test}, "take after resolving backpresure"). 94 | -------------------------------------------------------------------------------- /tests/multi2_stream.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% @author Evangelos Pappas 4 | %%% @copyright (C) 2015, evalonlabs 5 | %%% The MIT License (MIT) 6 | %%% 7 | %%% Copyright (c) 2015 Evangelos Pappas 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in all 17 | %%% copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | %%% SOFTWARE. 26 | %%% @doc 27 | %%% 28 | %%% @end 29 | %%%------------------------------------------------------------------- 30 | 31 | -module(multi2_stream). 32 | -author("epappas"). 33 | 34 | -behaviour(gen_stream). 35 | 36 | %% API 37 | 38 | %% gen_stream callbacks 39 | -export([init/1, on_data/3, on_offer/3, on_state/3]). 40 | 41 | -define(SERVER, ?MODULE). 42 | 43 | %%%=================================================================== 44 | %%% API 45 | %%%=================================================================== 46 | 47 | init(_Args) -> {ok, {}}. 48 | 49 | on_data(_Resource, Stream, State) -> 50 | {ok, Stream, State}. 51 | 52 | on_offer(Resource, Stream, State) -> 53 | {Resource * 2, Stream, State}. 54 | 55 | on_state(State, _Stream, StateData) -> 56 | {ok, StateData}. 57 | 58 | %%%=================================================================== 59 | %%% Internal functions 60 | %%%=================================================================== 61 | -------------------------------------------------------------------------------- /tests/pipe_test.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%% -*- erlang -*- 3 | %%! -pa ./ebin -pa ./tests 4 | %%%------------------------------------------------------------------- 5 | %%% @author Evangelos Pappas 6 | %%% @copyright (C) 2015, evalonlabs 7 | %%% The MIT License (MIT) 8 | %%% 9 | %%% Copyright (c) 2015 Evangelos Pappas 10 | %%% 11 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 12 | %%% of this software and associated documentation files (the "Software"), to deal 13 | %%% in the Software without restriction, including without limitation the rights 14 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | %%% copies of the Software, and to permit persons to whom the Software is 16 | %%% furnished to do so, subject to the following conditions: 17 | %%% 18 | %%% The above copyright notice and this permission notice shall be included in all 19 | %%% copies or substantial portions of the Software. 20 | %%% 21 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | %%% SOFTWARE. 28 | %%% @doc 29 | %%% 30 | %%% @end 31 | %%%------------------------------------------------------------------- 32 | 33 | -include("../src/erl_streams_commons.hrl"). 34 | 35 | main(_) -> 36 | etap:plan(unknown), 37 | 38 | test_pipe(), 39 | 40 | etap:end_tests(), 41 | ok. 42 | 43 | test_pipe() -> 44 | {ok, AddStreamPID} = gen_stream:start(add2_stream, add2_stream, []), 45 | {ok, MultiStream1PID} = gen_stream:start(multi2_stream, multi2_stream, []), 46 | {ok, MultiStream2PID} = gen_stream:start(multi2_stream, multi2_stream, []), 47 | 48 | gen_stream:pipe(AddStreamPID, MultiStream1PID), 49 | gen_stream:pipe(MultiStream1PID, MultiStream2PID), 50 | 51 | etap:is_ok(gen_stream:put(AddStreamPID, 3), "gen_stream:put to add2_stream should return ok"), 52 | 53 | % 20 = (3 + 2) * 2 * 2 54 | 55 | etap:is(gen_stream:take(MultiStream2PID), {ok, 20}, "gen_stream:take from multi2_stream should take the modified resource"). 56 | -------------------------------------------------------------------------------- /tests/simple_stream.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% @author Evangelos Pappas 4 | %%% @copyright (C) 2015, evalonlabs 5 | %%% The MIT License (MIT) 6 | %%% 7 | %%% Copyright (c) 2015 Evangelos Pappas 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in all 17 | %%% copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | %%% SOFTWARE. 26 | %%% @doc 27 | %%% 28 | %%% @end 29 | %%%------------------------------------------------------------------- 30 | 31 | -module(simple_stream). 32 | -author("epappas"). 33 | 34 | -behaviour(gen_stream). 35 | 36 | %% API 37 | 38 | %% gen_stream callbacks 39 | -export([init/1, on_data/3, on_offer/3, on_state/3]). 40 | 41 | -define(SERVER, ?MODULE). 42 | 43 | -define(OPEN, open). 44 | -define(DROPPING, dropping). 45 | -define(PAUSED, paused). 46 | -define(STOPPED, stopped). 47 | -define(CLOSED, closed). 48 | 49 | %%%=================================================================== 50 | %%% API 51 | %%%=================================================================== 52 | 53 | init(Args) -> 54 | etap:is(Args, [test], "should have proper args"), 55 | {ok, Args}. 56 | 57 | on_data(Resource, Stream, State) -> 58 | etap:is(Resource, test, "should receive test"), 59 | {ok, Stream, State}. 60 | 61 | on_offer(Resource, Stream, State) -> 62 | etap:is(Resource, test, "should offer test"), 63 | {1, Stream, State}. 64 | 65 | on_state(State, _Stream, StateData) -> 66 | etap:is_ok(lists:member(State, [ 67 | ?OPEN, 68 | ?DROPPING, 69 | ?PAUSED, 70 | ?STOPPED, 71 | ?CLOSED 72 | ]), "Should receive a valid state"), 73 | {ok, StateData}. 74 | 75 | %%%=================================================================== 76 | %%% Internal functions 77 | %%%=================================================================== 78 | -------------------------------------------------------------------------------- /tests/simple_stream.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%% -*- erlang -*- 3 | %%! -pa ./ebin -pa ./tests 4 | %%%------------------------------------------------------------------- 5 | %%% @author Evangelos Pappas 6 | %%% @copyright (C) 2015, evalonlabs 7 | %%% The MIT License (MIT) 8 | %%% 9 | %%% Copyright (c) 2015 Evangelos Pappas 10 | %%% 11 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 12 | %%% of this software and associated documentation files (the "Software"), to deal 13 | %%% in the Software without restriction, including without limitation the rights 14 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | %%% copies of the Software, and to permit persons to whom the Software is 16 | %%% furnished to do so, subject to the following conditions: 17 | %%% 18 | %%% The above copyright notice and this permission notice shall be included in all 19 | %%% copies or substantial portions of the Software. 20 | %%% 21 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | %%% SOFTWARE. 28 | %%% @doc 29 | %%% 30 | %%% @end 31 | %%%------------------------------------------------------------------- 32 | 33 | -include("../src/erl_streams_commons.hrl"). 34 | 35 | main(_) -> 36 | etap:plan(unknown), 37 | 38 | test_simple_start(), 39 | test_callbacks(), 40 | 41 | etap:end_tests(), 42 | ok. 43 | 44 | test_simple_start() -> 45 | {ok, StreamPID} = gen_stream:start(simple_stream, simple_stream, [test]), 46 | 47 | #stream{ 48 | mod = Mod, 49 | mod_state = State 50 | } = gen_stream:get_stream(StreamPID), 51 | 52 | etap:is(Mod, simple_stream, "new Stream should own simple_stream"), 53 | etap:is(State, [test], "new Stream should have correct state"). 54 | 55 | test_callbacks() -> 56 | {ok, StreamPID} = gen_stream:start(simple_stream, simple_stream, [test]), 57 | 58 | etap:is_ok(gen_stream:put(StreamPID, test), "gen_stream:put should return ok"), 59 | 60 | etap:is(gen_stream:take(StreamPID), {ok, 1}, "gen_stream:take should take the modified resource"). 61 | -------------------------------------------------------------------------------- /tests/stream.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%% -*- erlang -*- 3 | %%! -pa ./ebin -pa ./tests 4 | %%%------------------------------------------------------------------- 5 | %%% @author Evangelos Pappas 6 | %%% @copyright (C) 2015, evalonlabs 7 | %%% The MIT License (MIT) 8 | %%% 9 | %%% Copyright (c) 2015 Evangelos Pappas 10 | %%% 11 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 12 | %%% of this software and associated documentation files (the "Software"), to deal 13 | %%% in the Software without restriction, including without limitation the rights 14 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | %%% copies of the Software, and to permit persons to whom the Software is 16 | %%% furnished to do so, subject to the following conditions: 17 | %%% 18 | %%% The above copyright notice and this permission notice shall be included in all 19 | %%% copies or substantial portions of the Software. 20 | %%% 21 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | %%% SOFTWARE. 28 | %%% @doc 29 | %%% 30 | %%% @end 31 | %%%------------------------------------------------------------------- 32 | 33 | -include("../src/erl_streams_commons.hrl"). 34 | 35 | main(_) -> 36 | etap:plan(unknown), 37 | Stream = stream:new(), 38 | #stream{name = Name} = Stream, 39 | 40 | %% Basic dummy tests 41 | etap:is(Name =:= new_stream, true, "Anonymous Stream is named correctly"), 42 | etap:is(stream:is_empty(Stream), true, "New Streams should be empty"), 43 | 44 | %% TEST simple put 45 | {ok, StreamA0} = stream:put(Stream, 0), 46 | {_StreamA1, AA} = stream:take(StreamA0), 47 | 48 | etap:is(AA, AA, "Simple take and put case"), 49 | 50 | %% TEST list insertion 51 | {ok, StreamB0} = stream:put_from_list(Stream, [1, 2, 3]), 52 | 53 | {StreamB1, BA} = stream:take(StreamB0), 54 | {StreamB2, BB} = stream:take(StreamB1), 55 | {_StreamB3, BC} = stream:take(StreamB2), 56 | 57 | etap:is([BA, BB, BC], [1, 2, 3], "Take should get the elements from the inserted list"), 58 | 59 | test_dropping(), 60 | 61 | test_dropping_while(), 62 | 63 | test_map(), 64 | 65 | test_filter(), 66 | 67 | test_reduce(), 68 | 69 | etap:end_tests(), 70 | ok. 71 | 72 | test_dropping() -> 73 | Stream = stream:new(), 74 | 75 | {ok, Stream1} = stream:put(Stream, 0), 76 | 77 | Stream2 = stream:drop(Stream1), %% will drop the next one 78 | 79 | etap:is(stream:is_dropping(Stream2), true, "Stream should be in dropping state"), 80 | 81 | {ok, Stream3} = stream:put(Stream2, 1), 82 | 83 | {ok, Stream4} = stream:put(Stream3, 2), %% this action should unblock the stream 84 | 85 | etap:is(stream:is_dropping(Stream4), false, "Stream should escape dropping state after 1 put"), 86 | 87 | #stream{buffer = Buffer} = Stream4, 88 | 89 | etap:is(Buffer, [0, 2], "One message should be missing"). 90 | 91 | test_dropping_while() -> 92 | Stream = stream:new(), 93 | 94 | %% Initial put with non dropping condition 95 | {ok, Stream1} = stream:put(Stream, 0), 96 | 97 | Stream2 = stream:drop_while(Stream1, 98 | fun(#stream{} = _Stream, Resource) -> 99 | case Resource of 100 | 5 -> false; 101 | _ -> true 102 | end 103 | end 104 | ), %% will drop while 5 is not given 105 | 106 | etap:is(stream:is_dropping(Stream2), true, "Stream should be in dropping state"), 107 | 108 | {ok, Stream3} = stream:put_from_list(Stream2, [1, 2, 3, 4, 5, 6, 7, 8, 9]), 109 | 110 | etap:is(stream:is_dropping(Stream3), false, "Stream have stoped dropping"), 111 | 112 | #stream{buffer = Buffer} = Stream3, 113 | 114 | etap:is(Buffer, [0, 5, 6, 7, 8, 9], "All non valid messages should be missing"). 115 | 116 | test_map() -> 117 | Stream = stream:new(), 118 | 119 | Stream1 = stream:map(Stream, 120 | fun(Resource, _Buffer) -> 121 | Resource + 1 122 | end 123 | ), 124 | 125 | {ok, Stream2} = stream:put(Stream1, 0), 126 | {ok, Stream3} = stream:put(Stream2, 1), 127 | {ok, Stream4} = stream:put(Stream3, 2), 128 | 129 | #stream{buffer = Buffer} = Stream4, 130 | 131 | etap:is(Buffer, [1, 2, 3], "Map fun should have alter the values"). 132 | 133 | test_filter() -> 134 | Stream = stream:new(), 135 | 136 | Stream1 = stream:filter(Stream, 137 | fun(Resource, _Buffer) -> 138 | Resource =:= 1 139 | end 140 | ), 141 | 142 | {ok, Stream2} = stream:put(Stream1, 0), 143 | {ok, Stream3} = stream:put(Stream2, 1), 144 | {ok, Stream4} = stream:put(Stream3, 2), 145 | 146 | #stream{buffer = Buffer} = Stream4, 147 | 148 | etap:is(Buffer, [1], "Filter() should have filter the input"). 149 | 150 | test_reduce() -> 151 | Stream = stream:new(), 152 | 153 | Stream1 = stream:reduce(Stream, 154 | fun(Acc, Resource, _Buffer) -> 155 | math:pow(Acc, Resource) 156 | end, 157 | 2 158 | ), 159 | 160 | {ok, Stream2} = stream:put(Stream1, 1), 161 | {ok, Stream3} = stream:put(Stream2, 2), 162 | {ok, Stream4} = stream:put(Stream3, 3), 163 | 164 | #stream{buffer = Buffer, reduce_acc = Acc} = Stream4, 165 | 166 | etap:is(Buffer, [64.0], "Values should have been reduced"), 167 | 168 | etap:is(Acc, 64.0, "Accumulator should be updated"). 169 | --------------------------------------------------------------------------------