├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── core.ex └── core │ ├── app.ex │ ├── debug.ex │ ├── debug │ └── supervisor.ex │ ├── register.ex │ └── sys.ex ├── mix.exs └── test ├── core ├── debug_test.exs └── sys_test.exs ├── core_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 17.1 4 | before_install: 5 | - wget https://github.com/elixir-lang/elixir/releases/download/v1.0.0/Precompiled.zip -O elixir.zip 6 | - unzip -d elixir elixir.zip 7 | - export PATH=$(pwd)/elixir/bin:$PATH 8 | - epmd -daemon 9 | - mix local.hex --force 10 | script: 11 | - mix test 12 | notifications: 13 | recipients: 14 | - james@fishcakez.com 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Core 2 | Library for implementing OTP processes natively in Elixir. 3 | 4 | Provides utility functions and macros to implement 100% OTP compliant 5 | processes with 100% compatibility with all Erlang/OTP modules and tools. 6 | 7 | # Installing 8 | ``` 9 | git clone https://github.com/fishcakez/core.git 10 | cd core 11 | mix do deps.get, docs, compile 12 | ``` 13 | 14 | # Hello World 15 | Start a process that prints "Hello World" to `:stdio`. 16 | ```elixir 17 | defmodule HelloWorld do 18 | 19 | use Core 20 | 21 | def start_link(), do: Core.start_link(__MODULE__, nil) 22 | 23 | def init(_parent, _debug, _args) do 24 | IO.puts("Hello World") 25 | # Core.init_ack/0 will cause start_link/0 to return { :ok, self() }. If this 26 | # function is never called start_link will block until this process exits. 27 | Core.init_ack() 28 | exit(:normal) 29 | end 30 | 31 | end 32 | ``` 33 | 34 | # Features 35 | * Asynchronous and synchronous process initiation (with name registration). 36 | * Automatic logging of un-rescued exceptions. 37 | * System calls that work with any OTP compliant process. 38 | * Receive macro to handle system messages. 39 | * Supports progressive enhancement of OTP features: system message 40 | automatically handled until you want to change the default behaviour. 41 | 42 | # Basic Ping Server 43 | Starts a process that can be pinged. 44 | ```elixir 45 | defmodule PingPong do 46 | 47 | use Core 48 | 49 | @spec ping(Core.t) :: :pong 50 | def ping(process), do: Core.call(process, __MODULE__, :ping, 5000) 51 | 52 | @spec count(Core.t) :: non_neg_integer 53 | def count(process), do: Core.call(process, __MODULE__, :count, 5000) 54 | 55 | @spec close(Core.t) :: :ok 56 | def close(process), do: Core.call(process, __MODULE__, :close, 5000) 57 | 58 | @spec start_link() :: { :ok, pid } 59 | def start_link() do 60 | Core.start_link(__MODULE__, nil) 61 | end 62 | 63 | # Core api 64 | 65 | def init(_parent, _debug, _args) do 66 | Core.init_ack() 67 | loop(0) 68 | end 69 | 70 | ## Internal 71 | 72 | defp loop(count) do 73 | receive do 74 | { __MODULE__, from, :ping } -> 75 | Core.reply(from, :pong) 76 | loop(count + 1) 77 | { __MODULE__, from, :count } -> 78 | Core.reply(from, count) 79 | loop(count) 80 | { __MODULE__, from, :close } -> 81 | Core.reply(from, :ok) 82 | terminate(:normal) 83 | end 84 | end 85 | 86 | defp terminate(reason) do 87 | exit(reason) 88 | end 89 | 90 | end 91 | ``` 92 | 93 | # Advanced Ping Server 94 | Starts a process that can be pinged, live debugged and live code 95 | upgraded. 96 | 97 | For example `Core.Sys.set_state(pid, 0)` will reset the `count` to `0`. 98 | ```elixir 99 | 100 | defmodule PingPong do 101 | 102 | use Core.Sys 103 | 104 | @spec ping(Core.t) :: :pong 105 | def ping(process), do: Core.call(process, __MODULE__, :ping, 5000) 106 | 107 | @spec count(Core.t) :: non_neg_integer 108 | def count(process), do: Core.call(process, __MODULE__, :count, 5000) 109 | 110 | @spec close(Core.t) :: :ok 111 | def close(process), do: Core.call(process, __MODULE__, :close, 5000) 112 | 113 | # die/1 will print alot of information because the exit reason is abnormal. 114 | @spec die(Core.t) :: :ok 115 | def die(process), do: Core.call(process, __MODULE__, :die, 5000) 116 | 117 | @spec start_link() :: { :ok, pid } 118 | def start_link() do 119 | Core.start_link(nil, __MODULE__, nil, 120 | [{ :debug, [{ :log, 10 }, { :stats, true }] }]) 121 | end 122 | 123 | ## Core api 124 | 125 | def init(parent, debug, _args) do 126 | Core.init_ack() 127 | loop(0, parent, debug) 128 | end 129 | 130 | ## Core.Sys (minimal) api 131 | 132 | def system_continue(count, parent, debug), do: loop(count, parent, debug) 133 | 134 | def system_terminate(count, parent, debug, reason) do 135 | terminate(count, parent, debug, reason) 136 | end 137 | 138 | ## Internal 139 | 140 | defp loop(count, parent, debug) do 141 | Core.Sys.receive(__MODULE__, count, parent, debug) do 142 | { __MODULE__, from, :ping } -> 143 | # It is not required to record events using `Core.Debug.event/1` but is 144 | # a useful debug feature that is compiled to a no-op in production. 145 | debug = Core.Debug.event(debug, { :in, :ping, elem(from, 0) }) 146 | Core.reply(from, :pong) 147 | debug = Core.Debug.event(debug, { :out, :pong, elem(from, 0) }) 148 | count = count + 1 149 | debug = Core.Debug.event(debug, { :count, count }) 150 | loop(count, parent, debug) 151 | { __MODULE__, from, :count } -> 152 | debug = Core.Debug.event(debug, { :in, :count, elem(from, 0) }) 153 | Core.reply(from, count) 154 | debug = Core.Debug.event(debug, { :out, count, elem(from, 0) }) 155 | loop(count, parent, debug) 156 | { __MODULE__, from, :close } -> 157 | debug = Core.Debug.event(debug, { :in, :close, elem(from, 0) }) 158 | Core.reply(from, :ok) 159 | debug = Core.Debug.event(debug, { :out, :ok, elem(from, 0) }) 160 | terminate(count, parent, debug, :normal) 161 | { __MODULE__, from, :die } -> 162 | debug = Core.Debug.event(debug, { :in, :die, elem(from, 0) }) 163 | Core.reply(from, :ok) 164 | debug = Core.Debug.event(debug, { :out, :ok, elem(from, 0) }) 165 | terminate(count, parent, debug, :die) 166 | end 167 | end 168 | 169 | defp terminate(count, parent, debug, reason) do 170 | event = { :EXIT, reason } 171 | debug = Core.Debug.event(debug, event) 172 | Core.stop(__MODULE__, count, parent, debug, reason, event) 173 | end 174 | 175 | end 176 | ``` 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /lib/core.ex: -------------------------------------------------------------------------------- 1 | defmodule Core do 2 | @moduledoc """ 3 | Functions for handling process initiation, communication and termination. 4 | 5 | This module provides the basic features required for building OTP compliant 6 | processes. Many functions in this module are only intended for use in 7 | processes initiated by functions in this module. 8 | 9 | ## Examples 10 | 11 | defmodule Core.HelloWorld do 12 | 13 | use Core 14 | 15 | @spec start_link() :: { :ok, pid } 16 | def start_link(), do: Core.start_link(__MODULE__, nil) 17 | 18 | def init(_parent, _debug, nil) do 19 | Core.init_ack() 20 | IO.puts("Hello World!") 21 | end 22 | 23 | end 24 | 25 | defmodule Core.Fn do 26 | 27 | use Core 28 | 29 | @spec start_link((() -> any)) :: { :ok, pid } 30 | def start_link(fun), do: Core.start_link(__MODULE__, fun) 31 | 32 | @spec spawn_link((() -> any)) :: pid 33 | def spawn_link(fun), do: Core.spawn_link(__MODULE__, fun) 34 | 35 | def init(parent, debug, fun) when is_function(fun, 0) do 36 | Core.init_ack() 37 | try do 38 | fun.() 39 | rescue 40 | exception -> 41 | reason = { exception, System.stacktrace() } 42 | Core.stop(__MODULE__, fun, parent, debug, reason) 43 | catch 44 | :throw, value -> 45 | exception = Core.UncaughtThrowError[actual: value] 46 | reason = { exception, System.stacktrace() } 47 | Core.stop(__MODULE__, fun, parent, debug, reason) 48 | end 49 | end 50 | 51 | end 52 | 53 | defmodule Core.PingPong do 54 | 55 | use Core 56 | 57 | @spec ping(Core.t) :: :pong 58 | def ping(process), do: Core.call(process, __MODULE__, :ping, 5000) 59 | 60 | @spec count(Core.t) :: non_neg_integer 61 | def count(process), do: Core.call(process, __MODULE__, :count, 5000) 62 | 63 | @spec close(Core.t) :: :ok 64 | def close(process), do: Core.call(process, __MODULE__, :close, 5000) 65 | 66 | @spec start_link() :: { :ok, pid } 67 | def start_link(), do: Core.start_link(__MODULE__, nil) 68 | 69 | def init(_parent, _debug, nil) do 70 | Core.init_ack() 71 | loop(0) 72 | end 73 | 74 | defp loop(count) do 75 | receive do 76 | { __MODULE__, from, :ping } -> 77 | Core.reply(from, :pong) 78 | loop(count + 1) 79 | { __MODULE__, from, :count } -> 80 | Core.reply(from, count) 81 | loop(count) 82 | { __MODULE__, from, :close } -> 83 | Core.reply(from, :ok) 84 | terminate(count) 85 | end 86 | end 87 | 88 | defp terminate(_count) do 89 | exit(:normal) 90 | end 91 | 92 | end 93 | 94 | `Core.Debug` and `Core.Sys` contain more advanced versions of the last 95 | example. 96 | """ 97 | 98 | use Behaviour 99 | 100 | defcallback init(parent, Core.Debug.t, args) :: no_return 101 | 102 | defmacro __using__(_options) do 103 | quote location: :keep do 104 | 105 | @behaviour Core 106 | require Core.Debug 107 | 108 | @doc false 109 | def init(_parent, _debug, _args) do 110 | Core.init_ignore() 111 | end 112 | 113 | defoverridable [init: 3] 114 | 115 | end 116 | end 117 | 118 | ## start/spawn types 119 | 120 | @type local_name :: atom() 121 | @type global_name :: { :global, any } 122 | @type via_name :: { :via, module, any } 123 | @type name :: nil | local_name | global_name | via_name 124 | @typep args :: any 125 | @type option :: { :timeout, timeout } | { :debug, [Core.Debug.option] } | 126 | { :spawn_opt, [Process.spawn_opt] } 127 | @type start_return :: { :ok, pid } | { :ok, pid, any } | :ignore | 128 | { :error, any } 129 | 130 | ## communication types 131 | 132 | @type process :: pid | { local_name, node } 133 | @type t :: name | process 134 | @type label :: any 135 | @typep request :: any 136 | @opaque tag :: reference 137 | @type from :: { pid, tag } 138 | @typep response :: any 139 | 140 | ## general types 141 | 142 | @typep extra :: any 143 | @typep reason :: Exception.t | { Exception.t, Exception.stacktrace } | atom | 144 | { atom, any } 145 | @typep state :: any 146 | @type parent :: pid 147 | 148 | ## start/spawn api 149 | 150 | @doc """ 151 | Start a parentless Core process without a link. 152 | 153 | This function will block until the created processes sends an acknowledgement, 154 | exits or the timeout is reached. If the timeout is reached the created process 155 | is killed and `{ :error, :timeout }` will be returned. 156 | 157 | A process created with this function will automatically send error reports to 158 | the error logger if an exception is raised (and not rescued). A crash report 159 | is also sent when the process exits with an abnormal reason. 160 | 161 | To atomically spawn and link to the process include the `:spawn_opt` option 162 | `:link`. 163 | """ 164 | @spec start(module, args, [option]) :: start_return 165 | def start(mod, args, opts \\ []) do 166 | name = reg_name(opts) 167 | spawn_opts = Keyword.get(opts, :spawn_opt, []) 168 | timeout = Keyword.get(opts, :timeout, :infinity) 169 | init_args = [mod, name, nil, args, self(), opts] 170 | :proc_lib.start(__MODULE__, :init, init_args, timeout, spawn_opts) 171 | end 172 | 173 | @doc """ 174 | Start a Core process with a link. 175 | 176 | This function will block until the created processes sends an acknowledgement, 177 | exits or the timeout is reached. If the timeout is reached the created process 178 | is killed and `{ :error, :timeout }` will be returned. 179 | 180 | A process created with this function will automatically send error reports to 181 | the error logger if an exception is raised (and not rescued). A crash report 182 | is also sent when the process exits with an abnormal reason. 183 | 184 | A processes created with this function should exit when its parent does and 185 | with the same reason. 186 | """ 187 | @spec start_link(module, args, [option]) :: start_return 188 | def start_link(mod, args, opts \\ []) do 189 | name = reg_name(opts) 190 | spawn_opts = Keyword.get(opts, :spawn_opt, []) 191 | init_args = [mod, name, self(), args, self(), opts] 192 | timeout = Keyword.get(opts, :timeout, :infinity) 193 | :proc_lib.start_link(__MODULE__, :init, init_args, timeout, spawn_opts) 194 | end 195 | 196 | @doc """ 197 | Spawn a parentless Core process without a link. 198 | 199 | A process created with this function will automatically send error reports to 200 | the error logger if an exception is raised (and not rescued). A crash report 201 | is also sent when the process exits with an abnormal reason. 202 | 203 | To atomically spawn and link to the process include the `:spawn_opt` option 204 | `:link`. 205 | """ 206 | @spec spawn(module, args, [option]) :: pid 207 | def spawn(mod, args, opts \\ []) do 208 | name = reg_name(opts) 209 | spawn_opts = Keyword.get(opts, :spawn_opt, []) 210 | init_args = [mod, name, nil, args, nil, opts] 211 | :proc_lib.spawn_opt(__MODULE__, :init, init_args, spawn_opts) 212 | end 213 | 214 | @doc """ 215 | Spawn a Core process with a link. 216 | 217 | A process created with this function will automatically send error reports to 218 | the error logger if an exception is raised (and not rescued). A crash report 219 | is also sent when the process exits with an abnormal reason. 220 | 221 | A processes created with this function should exit when its parent does and 222 | with the same reason. 223 | """ 224 | @spec spawn_link(module, any, [option]) :: pid 225 | def spawn_link(mod, args, opts \\ []) do 226 | name = reg_name(opts) 227 | spawn_opts = Keyword.get(opts, :spawn_opt, []) 228 | init_args = [mod, name, self(), args, nil, opts] 229 | :proc_lib.spawn_opt(__MODULE__, :init, init_args, [:link | spawn_opts]) 230 | end 231 | 232 | 233 | @doc """ 234 | Sends an acknowledgment to the starter process. 235 | 236 | The starter process will block until it receives the acknowledgement. The 237 | start function will return `{ :ok, pid }`. 238 | 239 | If the process was created using a spawn function no acknowledgment is sent. 240 | 241 | This function is only intended for use by processes created by this module. 242 | """ 243 | @spec init_ack() :: :ok 244 | def init_ack() do 245 | case get_starter() do 246 | starter when starter === self() -> :ok 247 | starter -> :proc_lib.init_ack(starter, { :ok, self() }) 248 | end 249 | end 250 | 251 | @doc """ 252 | Sends an acknowledgment to the starter process with extra information. 253 | 254 | The starter process will block until it receives the acknowledgement. The 255 | start function will return `{ :ok, pid, extra }`. 256 | 257 | If the process was created using a spawn function no acknowledgment is sent. 258 | 259 | This function is only intended for use by processes created by this module. 260 | """ 261 | @spec init_ack(extra) :: :ok 262 | def init_ack(extra) do 263 | case get_starter() do 264 | starter when starter === self() -> :ok 265 | starter -> :proc_lib.init_ack(starter, { :ok, self(), extra }) 266 | end 267 | end 268 | 269 | @doc """ 270 | Sends an acknowledgment to the starter process to ignore the current process. 271 | 272 | This function is an alternative to `init_ack/0` and should be used be signal 273 | that nothing will occur in the created process. 274 | 275 | Before sending the acknowledgment the process will unregister any name that 276 | was associated with the process during initiation. 277 | 278 | After sending the acknowledgment the process will exit with reason `:normal` 279 | so this function should only be used as a tail call. 280 | 281 | The starter process will block until it receives the acknowledgement. The 282 | start function will return `:ignore`. 283 | 284 | If the process was created using a spawn function no acknowledgment is sent. 285 | 286 | This function is only intended for use by processes created by this module. 287 | """ 288 | @spec init_ignore :: no_return 289 | def init_ignore() do 290 | unregister() 291 | starter = get_starter() 292 | if starter !== self(), do: :proc_lib.init_ack(starter, :ignore) 293 | exit(:normal) 294 | end 295 | 296 | @doc """ 297 | Sends an acknowledgment to the starter process that the current process failed 298 | to initiate. 299 | 300 | This function is an alternative to `init_ack/0` and should be used be signal 301 | the rason why initiation failed. 302 | 303 | Before sending the acknowledgment the process may print debug information and 304 | log an error with the error logger. If the reason is of the form: 305 | `{ exception, stacktrace }`, the error will note that an exception was raised 306 | format the exception and stacktrace. Also any name that was associated with 307 | the process during initiation will be unregistered. 308 | 309 | After sending the acknowledgment the process will exit with the reason passed. 310 | As this function exits it should only be used as a tail call. 311 | 312 | The starter process will block until it receives the acknowledgement. The 313 | start function will return `{ :error, reason }`, where reason is the same 314 | reason as exiting. 315 | 316 | If the process was created using a spawn function no acknowledgment is sent. 317 | 318 | This function is only intended for use by processes created by this module. 319 | """ 320 | @spec init_stop(module, parent, Core.Debug.t, args, reason, 321 | Core.Debug.event) :: no_return 322 | def init_stop(mod, parent, debug, args, reason, event \\ nil) 323 | 324 | def init_stop(mod, parent, debug, args, reason, event) do 325 | type = exit_type(reason) 326 | starter = get_starter() 327 | if starter === self() and type === :abnormal do 328 | report_init_stop(mod, parent, args, reason, event) 329 | end 330 | if type === :abnormal, do: Core.Debug.print(debug) 331 | unregister() 332 | if starter !== self(), do: :proc_lib.init_ack(starter, { :error, reason }) 333 | exit(reason) 334 | end 335 | 336 | @doc """ 337 | Stops a Core process. 338 | 339 | Before exiting the process may print debug information and will send an error 340 | report to the error logger when the reason is not `:normal`, `:shutdown` or of 341 | the form `{ :shutdown, any }`. If the reason is of the form: 342 | `{ exception, stacktrace }`, the error will note that an exception was raised 343 | format the exception and stacktrace. A crash report with additional 344 | information will also be sent to the error logger. 345 | 346 | The process will exit with the reason passed. As this function exits it should 347 | only be used as a tail call. 348 | 349 | This function is only intended for use by processes created by this module. 350 | """ 351 | @spec stop(module, state, parent, Core.Debug.t, reason, 352 | Core.Debug.event) :: no_return 353 | def stop(mod, state, parent, debug, reason, event \\ nil) 354 | 355 | def stop(mod, state, parent, debug, reason, event) do 356 | type = exit_type(reason) 357 | if type === :abnormal do 358 | report_stop(mod, state, parent, reason, event) 359 | Core.Debug.print(debug) 360 | end 361 | exit(reason) 362 | end 363 | 364 | ## communication api 365 | 366 | @doc """ 367 | Returns the pid or `{ local_name, node }` of the Core process associated with 368 | the name or process. 369 | Returns `nil` if no process is associated with the name. 370 | 371 | The returned process may not be alive. 372 | """ 373 | @spec whereis(t) :: process | nil 374 | def whereis(pid) when is_pid(pid), do: pid 375 | def whereis(name) when is_atom(name), do: Process.whereis(name) 376 | 377 | def whereis({ :global, name }) do 378 | case :global.whereis_name(name) do 379 | :undefined -> 380 | nil 381 | pid -> 382 | pid 383 | end 384 | end 385 | 386 | def whereis({ name, node_name }) 387 | when is_atom(name) and node_name === node() do 388 | Process.whereis(name) 389 | end 390 | 391 | def whereis({ name, node_name } = process) 392 | when is_atom(name) and is_atom(node_name) do 393 | process 394 | end 395 | 396 | def whereis({ :via, mod, name }) when is_atom(mod) do 397 | case mod.whereis_name(name) do 398 | :undefined -> 399 | nil 400 | pid -> 401 | pid 402 | end 403 | end 404 | 405 | @doc """ 406 | Calls the Core process associated the with name or process and returns the 407 | reponse. 408 | 409 | The message is sent in the form `{ label, from, request }`. A response is sent 410 | by calling `reply(from, response)`. 411 | 412 | Catching an exit from this function may result in unexpected messages 413 | arriving in the calling processes mailbox. It is recommended to terminate soon 414 | after an exit is caught. 415 | """ 416 | @spec call(t, label, request, timeout) :: response 417 | def call(target, label, request, timeout) do 418 | case whereis(target) do 419 | pid when is_pid(pid) -> 420 | case safe_call(pid, label, request, timeout) do 421 | {:ok, response} -> 422 | response 423 | {:error, :noconnection} -> 424 | reason = {:nodedown, node(pid)} 425 | exit({reason, 426 | {__MODULE__, :call, [target, label, request, timeout]}}) 427 | {:error, reason} -> 428 | exit({reason, 429 | {__MODULE__, :call, [target, label, request, timeout]}}) 430 | end 431 | { local_name, node_name } = process 432 | when is_atom(local_name) and is_atom(node_name) -> 433 | case safe_call(process, label, request, timeout) do 434 | {:ok, response} -> 435 | response 436 | {:error, :noconnection} -> 437 | reason = {:nodedown, node_name} 438 | exit({reason, 439 | {__MODULE__, :call, [target, label, request, timeout]}}) 440 | {:error, reason} -> 441 | exit({reason, 442 | {__MODULE__, :call, [target, label, request, timeout]}}) 443 | end 444 | nil -> 445 | exit({:noproc, {__MODULE__, :call, [target, label, request, timeout]}}) 446 | end 447 | end 448 | 449 | @doc """ 450 | Sends a response to a call message. 451 | 452 | The first argument is the `from` term from a call message of the form: 453 | `{ label, from, request }`. 454 | """ 455 | @spec reply(from, response) :: response 456 | def reply({ to, tag }, response) do 457 | try do 458 | Kernel.send(to, { tag, response }) 459 | catch 460 | ArgumentError -> 461 | response 462 | end 463 | end 464 | 465 | @doc """ 466 | Sends a message to the Core process associated with the name or process. 467 | 468 | This function will not raise an exception if there is not a process associated 469 | with the name. 470 | 471 | This function will not block to attempt an connection to a disconnected node. 472 | Instead it will spawn a process to make the attempt and return `:ok` 473 | immediately. Therefore messages to processes on other nodes may not arrive 474 | out of order, if they are received. Messages to processes on the same node 475 | will arrive in order, if they are received. 476 | """ 477 | @spec cast(t, label, request) :: :ok 478 | def cast(target, label, request) do 479 | case whereis(target) do 480 | nil -> 481 | :ok 482 | process -> 483 | msg = { label, request } 484 | cast(process, msg) 485 | end 486 | end 487 | 488 | @doc """ 489 | Sends a message to the Core process associated with the name or process. 490 | 491 | This function will raise an ArgumentError if a name is provided and no process 492 | is associated with the name - unless the name is for a locally registered name 493 | on another node. Similar to the behaviour of `Kernel.send/2`. 494 | 495 | This function will block to attempt a connection to a disconnected node, and 496 | so messages sent by this function will arrive in order, if they are received. 497 | """ 498 | @spec send(t, request) :: request 499 | def send(target, msg) do 500 | case whereis(target) do 501 | nil -> 502 | raise ArgumentError, 503 | message: "no process associated with #{format(target)}" 504 | process -> 505 | Kernel.send(process, msg) 506 | end 507 | end 508 | 509 | ## hibernation api 510 | 511 | @doc """ 512 | Hibernates the Core process. Must be used to hibernate Core processes to 513 | ensure exceptions (and exits) are handled correctly. 514 | 515 | This function throws away the stack and should only be used as a tail call. 516 | 517 | When the process leaves hibernation the following will be called: 518 | `apply(mod, fun, [state, parent, debug] ++ args)` 519 | 520 | This function is only intended for use by processes created by this module. 521 | """ 522 | @spec hibernate(module, atom, state, parent, Core.Debug.t, [any]) :: no_return 523 | def hibernate(mod, fun, state, parent, debug, args \\ []) do 524 | :proc_lib.hibernate(__MODULE__, :continue, 525 | [mod, fun, state, parent, debug, args]) 526 | end 527 | 528 | ## :proc_lib api 529 | 530 | @doc false 531 | @spec init( nil | pid, nil | pid, name, module, args, [option]) :: 532 | no_return 533 | def init(mod, nil, parent, args, starter, opts) do 534 | init(mod, self(), parent, args, starter, opts) 535 | end 536 | 537 | def init(mod, name, nil, args, starter, opts) do 538 | init(mod, name, self(), args, starter, opts) 539 | end 540 | 541 | def init(mod, name, parent, args, nil, opts) do 542 | init(mod, name, parent, args, self(), opts) 543 | end 544 | 545 | def init(mod, name, parent, args, starter, opts) do 546 | try do 547 | do_init(mod, name, parent, args, starter, opts) 548 | else 549 | _ -> 550 | # Explicitly exit. 551 | exit(:normal) 552 | rescue 553 | exception -> 554 | base_stop(mod, parent, { exception, System.stacktrace() }) 555 | catch 556 | # Turn throw into the error it would be. 557 | :throw, value -> 558 | error = {:nocatch, value} 559 | base_stop(mod, parent, { error, System.stacktrace() }) 560 | # Exits are not caught as they are an explicit intention to exit. 561 | end 562 | end 563 | 564 | @doc false 565 | @spec continue(module, atom, state, parent, Core.Debug.t, args) :: no_return 566 | def continue(mod, fun, state, parent, debug, args) do 567 | try do 568 | apply(mod, fun, [state, parent, debug] ++ args) 569 | else 570 | _ -> 571 | # Explicitly exit. 572 | exit(:normal) 573 | rescue 574 | exception -> 575 | base_stop(mod, parent, { exception, System.stacktrace() }) 576 | catch 577 | # Turn throw into the error it would be. 578 | :throw, value -> 579 | error = {:nocatch, value} 580 | base_stop(mod, parent, { error, System.stacktrace() }) 581 | # Exits are not caught as they are an explicit intention to exit. 582 | end 583 | end 584 | 585 | ## utils 586 | 587 | @doc false 588 | @spec get_name() :: name 589 | def get_name(), do: Process.get(:"$name", self()) 590 | 591 | @doc false 592 | @spec format(t) :: String.t 593 | def format(proc \\ get_name()) 594 | 595 | def format(pid) when is_pid(pid) and node(pid) == node() do 596 | inspect(pid) 597 | end 598 | 599 | def format(pid) when is_pid(pid) do 600 | "#{inspect(pid)} on #{node(pid)}" 601 | end 602 | 603 | def format(name) when is_atom(name) do 604 | to_string(name) 605 | end 606 | 607 | def format({ :global, name }) do 608 | "#{inspect(name)} (global)" 609 | end 610 | 611 | def format({ name, node_name }) when node_name === node() do 612 | to_string(name) 613 | end 614 | 615 | def format({ name, node_name }) do 616 | "#{to_string(name)} on #{to_string(node_name)}" 617 | end 618 | 619 | def format({ :via, mod, name }) do 620 | "#{inspect(name)} (#{mod})" 621 | end 622 | 623 | ## internal 624 | 625 | ## init 626 | 627 | defp reg_name([{ :local, name } | _opts]) when is_atom(name), do: name 628 | defp reg_name([{ :global, _global_name } = name | _opts]), do: name 629 | 630 | defp reg_name([{ :via, { mod, via_name } } | _opts]) when is_atom(mod) do 631 | { :via, mod, via_name } 632 | end 633 | 634 | defp reg_name([{ key, _ } = name | _opts]) 635 | when key in [:local, :global, :via] do 636 | raise ArgumentError, message: "invalid name: #{inspect(name)}" 637 | end 638 | 639 | defp reg_name([_opt | opts]), do: reg_name(opts) 640 | 641 | defp reg_name([]), do: nil 642 | 643 | defp format_name(name \\ get_name()) 644 | 645 | defp format_name(pid) when is_pid(pid), do: pid 646 | defp format_name(name) when is_atom(name), do: name 647 | defp format_name({:global, name}), do: name 648 | defp format_name({:via, _mod, name}), do: name 649 | 650 | defp do_init(mod, name, parent, args, starter, opts) do 651 | case register(name) do 652 | :yes -> 653 | put_starter(starter) 654 | put_name(name) 655 | debug = new_debug(opts) 656 | mod.init(parent, debug, args) 657 | :no when starter === self() -> 658 | exit(:normal) 659 | :no -> 660 | reason = { :already_started, whereis(name) } 661 | :proc_lib.init_ack(starter, { :error, reason }) 662 | exit(:normal) 663 | end 664 | end 665 | 666 | defp register(pid) when is_pid(pid), do: :yes 667 | 668 | defp register(name) when is_atom(name) do 669 | try do 670 | Process.register(self(), name) 671 | else 672 | :true -> 673 | :yes 674 | rescue 675 | ArgumentError -> 676 | :no 677 | end 678 | end 679 | 680 | defp register({ :global, name }) do 681 | :global.register_name(name, self()) 682 | end 683 | 684 | defp register({ :via, mod, name }) when is_atom(mod) do 685 | mod.register_name(name, self()) 686 | end 687 | 688 | defp put_name(name), do: Process.put(:"$name", name) 689 | 690 | defp put_starter(starter), do: Process.put(:"$starter", starter) 691 | 692 | defp get_starter(), do: Process.get(:"$starter", self()) 693 | 694 | defp new_debug(opts) do 695 | case Keyword.get(opts, :debug, nil) do 696 | nil -> 697 | Core.Debug.new() 698 | debug_opts -> 699 | Core.Debug.new(debug_opts) 700 | end 701 | end 702 | 703 | ## stopping 704 | 705 | defp exit_type(:normal), do: :normal 706 | defp exit_type(:shutdown), do: :normal 707 | defp exit_type({ :shutdown, _reason }), do: :nornal 708 | defp exit_type(_reason), do: :abnormal 709 | 710 | defp unregister(name \\ get_name()) 711 | defp unregister(pid) when is_pid(pid), do: nil 712 | defp unregister(name) when is_atom(name), do: Process.unregister(name) 713 | defp unregister({ :global, name }), do: :global.unregister_name(name) 714 | defp unregister({ :via, mod, name }), do: mod.unregister_name(name) 715 | 716 | defp base_stop(mod, parent, reason) do 717 | report_base_stop(mod, parent, reason) 718 | exit(reason) 719 | end 720 | 721 | defp report_base_stop(mod, parent, reason) do 722 | erl_format = '~i** Core ~p is terminating~n** Module == ~p~n** Process == ~p~n** Parent == ~p~n** Reason for termination == ~n** ~p~n' 723 | args = [{__MODULE__, :stop}, format_name(), mod, self(), parent, reason] 724 | report(erl_format, args) 725 | end 726 | 727 | defp report_init_stop(mod, parent, args, reason, event) do 728 | erl_format = '~i** ~p ~p is terminating~n** Last event was ~p~n** Arguments == ~p~n** Process == ~p~n** Parent == ~p~n** Reason for termination == ~n** ~p~n' 729 | args = [{__MODULE__, mod, :init_stop}, mod, format_name(), event, args, self(), parent, 730 | reason] 731 | report(erl_format, args) 732 | end 733 | 734 | defp report_stop(mod, state, parent, reason, event) do 735 | erl_format = '~i** ~p ~p is terminating~n** Last event was ~p~n** State == ~p~n** Process == ~p~n** Parent == ~p~n** Reason for termination == ~n** ~p~n' 736 | args = [{__MODULE__, mod, :stop}, mod, format_name(), event, state, self(), parent, 737 | reason] 738 | report(erl_format, args) 739 | end 740 | 741 | defp report(erl_format, args), do: :error_logger.error_msg(erl_format, args) 742 | 743 | ## communication 744 | 745 | defp safe_call(process, label, request, timeout) do 746 | try do 747 | Process.monitor(process) 748 | rescue 749 | ArgumentError -> 750 | case node() do 751 | # Can't connect to other nodes 752 | :"nonode@nohost" -> 753 | {:error, :noconnection} 754 | # Target node is feature weak 755 | _other -> 756 | {:error, :nomonitor} 757 | end 758 | else 759 | tag -> 760 | Process.send(process, { label, { self(), tag }, request }, [:noconnect]) 761 | receive do 762 | { ^tag, response } -> 763 | Process.demonitor(tag, [:flush]) 764 | {:ok, response} 765 | { :DOWN, ^tag, _, _, reason } -> 766 | {:error, reason} 767 | after 768 | timeout -> 769 | Process.demonitor(tag, [:flush]) 770 | {:error, :timeout} 771 | end 772 | end 773 | end 774 | 775 | defp cast(process, msg) do 776 | try do 777 | Process.send(process, msg, [:noconnect]) 778 | else 779 | :noconnect -> 780 | Kernel.spawn(Process, :send, [process, msg]) 781 | :ok 782 | :ok -> 783 | :ok 784 | rescue 785 | _exception -> 786 | :ok 787 | end 788 | end 789 | 790 | end 791 | -------------------------------------------------------------------------------- /lib/core/app.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.App do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | def start(_type, _args) do 7 | Core.Debug.Supervisor.start_link() 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /lib/core/debug.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Debug do 2 | @moduledoc """ 3 | Functions for handling debug events and statistics. 4 | 5 | Start the VM with `--gen-debug` to turn on logging and statistics by default. 6 | 7 | The functions in this module are intended for use during testing and 8 | development. 9 | 10 | ## Examples 11 | 12 | defmodule Core.PingPong do 13 | 14 | use Core 15 | 16 | @spec ping(Core.t) :: :pong 17 | def ping(process), do: Core.call(process, __MODULE__, :ping, 5000) 18 | 19 | @spec count(Core.t) :: non_neg_integer 20 | def count(process), do: Core.call(process, __MODULE__, :count, 5000) 21 | 22 | @spec close(Core.t) :: :ok 23 | def close(process), do: Core.call(process, __MODULE__, :close, 5000) 24 | 25 | @spec start_link() :: { :ok, pid } 26 | def start_link() do 27 | Core.start_link(nil, __MODULE__, nil, 28 | [{ :debug, [{ :log, 10 }, { :stats, true }] }]) 29 | end 30 | 31 | def init(_parent, debug, nil) do 32 | Core.init_ack() 33 | loop(0, debug) 34 | end 35 | 36 | defp loop(count, debug) do 37 | receive do 38 | { __MODULE__, from, :ping } -> 39 | debug = Core.Debug.event(debug, { :in, :ping, elem(from, 0) }) 40 | Core.reply(from, :pong) 41 | debug = Core.Debug.event(debug, { :out, :pong, elem(from, 0) }) 42 | count = count + 1 43 | debug = Core.Debug.event(debug, { :count, count }) 44 | loop(count, debug) 45 | { __MODULE__, from, :count } -> 46 | debug = Core.Debug.event(debug, { :in, :count, elem(from, 0) }) 47 | Core.reply(from, count) 48 | debug = Core.Debug.event(debug, { :out, count, elem(from, 0) }) 49 | loop(count, debug) 50 | { __MODULE__, from, :close } -> 51 | debug = Core.Debug.event(debug, { :in, :close, elem(from, 0) }) 52 | Core.reply(from, :ok) 53 | debug = Core.Debug.event(debug, { :out, :ok, elem(from, 0) }) 54 | terminate(count, debug) 55 | end 56 | end 57 | 58 | defp terminate(count, debug) do 59 | debug = Core.Debug.event(debug, { :EXIT, :normal }) 60 | Core.Debug.print_stats(debug) 61 | Core.Debug.print_log(debug) 62 | exit(:normal) 63 | end 64 | 65 | end 66 | 67 | """ 68 | 69 | @type event :: any 70 | @type hook_state :: any 71 | @type hook :: 72 | ((hook_state, event, process_term :: any) -> :done | hook_state ) 73 | @type option :: { :trace, boolean } | { :log, non_neg_integer } | 74 | { :stats, boolean } | { :log_file, Path.t | nil } | 75 | { :hook, { hook, hook_state | nil } } 76 | @type t :: [:sys.dbg_opt] 77 | @type stats :: map 78 | @typep report :: ((IO.device, report, any) -> any) 79 | 80 | ## macros 81 | 82 | @doc """ 83 | Macro for handling debug events. 84 | 85 | The macro will become a no-op if Mix.env == :prod and no debug features will 86 | be carried out, even if the debug object has debugging enabled. 87 | 88 | When the debug object has no debugging features enabled the created code will 89 | not make any external calls and is nearly a no-op. 90 | 91 | Should only be used by the process that created the debug object. 92 | """ 93 | defmacro event([], _event), do: [] 94 | defmacro event(debug, event) do 95 | if handle_event?() do 96 | quote do 97 | case unquote(debug) do 98 | [] -> 99 | unquote(debug) 100 | _ -> 101 | :sys.handle_debug(unquote(debug), &Core.Debug.print_event/3, 102 | Core.get_name(), unquote(event)) 103 | end 104 | end 105 | else 106 | debug 107 | end 108 | end 109 | 110 | ## api 111 | 112 | @doc """ 113 | Creates a new debug object. 114 | 115 | `new/0` will use default debug options, with the `--gen-debug` VM option this 116 | will be `[:statistics, :log]` - unless overridden by `set_opts/1`. 117 | """ 118 | @spec new([option]) :: t 119 | def new(opts \\ get_opts()) do 120 | parse_options(opts) 121 | |> :sys.debug_options() 122 | end 123 | 124 | @doc """ 125 | Returns the default options. 126 | """ 127 | @spec get_opts() :: [option] 128 | def get_opts(), do: :ets.lookup_element(__MODULE__, :options, 2) 129 | 130 | @doc """ 131 | Sets the default options. 132 | """ 133 | @spec set_opts([option]) :: :ok 134 | def set_opts(opts) do 135 | true = :ets.update_element(__MODULE__, :options, { 2, opts }) 136 | :ok 137 | end 138 | 139 | @doc false 140 | @spec ensure_table() :: :ok 141 | def ensure_table() do 142 | if :ets.info(__MODULE__) === :undefined do 143 | :ets.new(__MODULE__, 144 | [:set, :public, :named_table, { :read_concurrency, true }]) 145 | set_default_options() 146 | end 147 | :ok 148 | end 149 | 150 | ## event 151 | 152 | @doc false 153 | @spec print_event(IO.device, event, Core.name) :: :ok 154 | def print_event(device, event, name) do 155 | header = "** Core.Debug #{Core.format(name)} " 156 | formatted_event = format_event(event) 157 | IO.puts(device, [header | formatted_event]) 158 | end 159 | 160 | ## logs 161 | 162 | @doc """ 163 | Returns a list of stored events in order. 164 | 165 | Should only be used by the process that created the debug object. 166 | """ 167 | @spec get_log(t) :: [event] 168 | def get_log(debug) do 169 | get_raw_log(debug) 170 | |> log_from_raw() 171 | end 172 | 173 | @doc """ 174 | Prints logged events to the device (defaults to :stdio). 175 | 176 | Should only be used by the process that created the debug object. 177 | """ 178 | @spec print_log(t, IO.device) :: :ok 179 | def print_log(debug, device \\ :stdio) do 180 | case get_raw_log(debug) do 181 | [] -> 182 | write_raw_log(device, []) 183 | raw_log -> 184 | write_raw_log(device, raw_log) 185 | end 186 | end 187 | 188 | ## stats 189 | 190 | @doc """ 191 | Returns a map of statistics. 192 | Returns nil if statstics is disabled. 193 | 194 | Should only be used by the process that created the debug object. 195 | """ 196 | @spec get_stats(t) :: stats | nil 197 | def get_stats(debug) do 198 | case get_raw_stats(debug) do 199 | :no_statistics -> 200 | nil 201 | raw_stats -> 202 | stats_from_raw(raw_stats) 203 | end 204 | end 205 | 206 | @doc """ 207 | Prints statistics to the device (defaults to :stdio). 208 | 209 | Should only be used by the process that created the debug object. 210 | """ 211 | @spec print_stats(t, IO.device) :: :ok 212 | def print_stats(debug, device \\ :stdio) do 213 | stats = get_stats(debug) 214 | write_stats(device, stats) 215 | end 216 | 217 | @doc """ 218 | Prints statistics and logs (if active) to the device (defaults to :stdio). 219 | 220 | Should only be used by the process that created the debug object 221 | """ 222 | @spec print(t, IO.device) :: :ok 223 | def print(debug, device \\ :stdio) do 224 | maybe_print_stats(debug, device) 225 | maybe_print_log(debug, device) 226 | end 227 | 228 | @doc false 229 | @spec get_raw_log(t) :: [{ event, any, report}] 230 | # return value used in get_status calls 231 | def get_raw_log(debug) do 232 | case :sys.get_debug(:log, debug, []) do 233 | [] -> 234 | [] 235 | { _max, rev_raw_log } -> 236 | Enum.reverse(rev_raw_log) 237 | end 238 | end 239 | 240 | @doc false 241 | @spec log_from_raw([{ event, any, report }]) :: [event] 242 | def log_from_raw(raw_log) do 243 | Enum.map(raw_log, fn({ event, _state, _report }) -> event end) 244 | end 245 | 246 | @doc false 247 | @spec write_raw_log(IO.device, Core.t, [{ event, any, report }]) :: :ok 248 | def write_raw_log(device, process \\ Core.get_name(), raw_log) 249 | 250 | 251 | def write_raw_log(device, process, []) do 252 | header = "** Core.Debug #{Core.format(process)} event log is empty\n" 253 | IO.puts(device, header) 254 | end 255 | 256 | def write_raw_log(device, process, raw_log) do 257 | formatted_log = format_raw_log(raw_log) 258 | header = "** Core.Debug #{Core.format(process)} event log:\n" 259 | IO.puts(device, [header | formatted_log]) 260 | end 261 | 262 | @doc false 263 | def get_raw_stats(debug) do 264 | case :sys.get_debug(:statistics, debug, :no_statistics) do 265 | :no_statistics -> 266 | :no_statistics 267 | { start_time, { :reductions, start_reductions }, msg_in, msg_out } -> 268 | current_time = :erlang.localtime() 269 | { :reductions, current_reductions } = Process.info(self(), :reductions) 270 | reductions = current_reductions - start_reductions 271 | [start_time: start_time, current_time: current_time, 272 | reductions: reductions, messages_in: msg_in, messages_out: msg_out] 273 | end 274 | end 275 | 276 | @doc false 277 | @spec stats_from_raw(:no_statistics | [{ atom, any}]) :: stats | nil 278 | def stats_from_raw(:no_statistics), do: nil 279 | 280 | def stats_from_raw(raw_stats) do 281 | stats = Enum.into(raw_stats, Map.new()) 282 | # rename messages_in/out for convenience 283 | { msg_in, stats } = Map.pop(stats, :messages_in) 284 | { msg_out, stats } = Map.pop(stats, :messages_out) 285 | Map.put(stats, :in, msg_in) 286 | |> Map.put(:out, msg_out) 287 | end 288 | 289 | @doc false 290 | def write_stats(device, process \\ Core.get_name(), stats) 291 | 292 | def write_stats(device, process, nil) do 293 | header = "** Core.Debug #{Core.format(process)} statistics not active\n" 294 | IO.puts(device, header) 295 | end 296 | 297 | def write_stats(device, process, stats) do 298 | header = "** Core.Debug #{Core.format(process)} statistics:\n" 299 | formatted_stats = format_stats(stats) 300 | IO.puts(device, [header | formatted_stats]) 301 | end 302 | 303 | ## internal 304 | 305 | ## options 306 | 307 | defp parse_options([]), do: [] 308 | 309 | defp parse_options(opts) do 310 | parse_trace([], opts) 311 | |> parse_log(opts) 312 | |> parse_stats(opts) 313 | |> parse_log_file(opts) 314 | |> parse_hooks(opts) 315 | end 316 | 317 | defp parse_trace(acc, opts) do 318 | case Keyword.get(opts, :trace, false) do 319 | true -> 320 | [:trace | acc] 321 | false -> 322 | acc 323 | end 324 | end 325 | 326 | defp parse_log(acc, opts) do 327 | case Keyword.get(opts, :log, 0) do 328 | 0 -> 329 | acc 330 | max when is_integer(max) and max > 0 -> 331 | [{ :log, max } | acc] 332 | end 333 | end 334 | 335 | defp parse_stats(acc, opts) do 336 | case Keyword.get(opts, :stats, false) do 337 | true -> 338 | [:statistics | acc] 339 | false -> 340 | acc 341 | end 342 | end 343 | 344 | defp parse_log_file(acc, opts) do 345 | case Keyword.get(opts, :log_file, nil) do 346 | nil -> 347 | acc 348 | path -> 349 | [ { :log_to_file, path } | acc ] 350 | end 351 | end 352 | 353 | defp parse_hooks(acc, opts) do 354 | Keyword.get_values(opts, :hook) 355 | |> Enum.reduce([], &add_hook/2) 356 | |> Enum.reduce(acc, &hook_options/2) 357 | end 358 | 359 | defp add_hook({hook, hook_state}, acc) when is_function(hook, 3) do 360 | case List.keymember?(acc, hook, 0) do 361 | true -> 362 | acc 363 | false -> 364 | [{ hook, hook_state } | acc] 365 | end 366 | end 367 | 368 | defp hook_options({ _hook, nil }, acc), do: acc 369 | defp hook_options(other, acc), do: [{ :install, other} | acc] 370 | 371 | defp set_default_options() do 372 | options = :application.get_env(:base, :options, []) 373 | case :init.get_argument(:generic_debug) do 374 | { :ok, _ } -> 375 | :ets.insert(__MODULE__, { :options, generic_debug(options) }) 376 | :error -> 377 | :ets.insert(__MODULE__, { :options, options }) 378 | end 379 | end 380 | 381 | defp generic_debug(options) do 382 | add_log(options) 383 | |> add_statistics() 384 | end 385 | 386 | defp add_log(options) do 387 | if Enum.any?(options, &log_option?/1) do 388 | options 389 | else 390 | [:log | options] 391 | end 392 | end 393 | 394 | defp log_option?(:log), do: true 395 | defp log_option?({ :log, _ }), do: true 396 | defp log_option?(_other), do: false 397 | 398 | defp add_statistics(options) do 399 | if Enum.member?(options, :statistics) or Enum.member?(options, :stats) do 400 | options 401 | else 402 | [:stats | options] 403 | end 404 | end 405 | 406 | ## event 407 | 408 | defp handle_event?() do 409 | cond do 410 | # Mix is not loaded, turn debugging on 411 | not :erlang.function_exported(Mix, :env, 0) -> 412 | true 413 | # prod(uction) mode, turn debugging off 414 | Mix.env === :prod -> 415 | false 416 | # Mix is loaded but not prod, turn debugging on 417 | true -> 418 | true 419 | end 420 | end 421 | 422 | defp format_event({ :in, msg, from }) do 423 | ["message in (from ", inspect(from), "): ", inspect(msg)] 424 | end 425 | 426 | defp format_event({ :in, msg }) do 427 | ["message in: " | inspect(msg)] 428 | end 429 | 430 | defp format_event({ :out, msg, to }) do 431 | ["message out (to ", inspect(to), "): ", inspect(msg)] 432 | end 433 | 434 | defp format_event(event), do: inspect(event) 435 | 436 | ## log 437 | 438 | # Collect log into a single binary so that it is written to the device in 439 | # one, rather than many seperate writes. With seperate writes the events 440 | # could be interwoven with other writes to the device. 441 | defp format_raw_log(raw_log) do 442 | { :ok, device } = StringIO.open(<<>>) 443 | try do 444 | format_raw_log(raw_log, device) 445 | else 446 | formatted_log -> 447 | formatted_log 448 | after 449 | StringIO.close(device) 450 | end 451 | end 452 | 453 | defp format_raw_log(raw_log, device) do 454 | Enum.each(raw_log, &print_raw_event(device, &1)) 455 | { input, output } = StringIO.contents(device) 456 | [input | output] 457 | end 458 | 459 | defp print_raw_event(device, { event, state, print }) do 460 | print.(device, event, state) 461 | end 462 | 463 | ## stats 464 | 465 | defp format_stats(%{ start_time: start_time, current_time: current_time, 466 | reductions: reductions, in: msg_in, out: msg_out }) do 467 | [" Start Time: #{format_time(start_time)}\n", 468 | " Current Time: #{format_time(current_time)}\n", 469 | " Messages In: #{msg_in}\n", 470 | " Messages Out: #{msg_out}\n", 471 | " Reductions: #{reductions}\n"] 472 | end 473 | 474 | defp format_time({ { year, month, day }, { hour, min, sec } }) do 475 | format = '~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B' 476 | args = [year, month, day, hour, min, sec] 477 | :io_lib.format(format, args) 478 | |> IO.iodata_to_binary() 479 | end 480 | 481 | ## print 482 | 483 | defp maybe_print_stats(debug, device) do 484 | case get_stats(debug) do 485 | nil -> 486 | :ok 487 | stats -> 488 | write_stats(device, stats) 489 | end 490 | end 491 | 492 | defp maybe_print_log(debug, device) do 493 | case :sys.get_debug(:log, debug, nil) do 494 | nil -> 495 | :ok 496 | { _max, raw_log } -> 497 | write_raw_log(device, Enum.reverse(raw_log)) 498 | end 499 | end 500 | 501 | end 502 | -------------------------------------------------------------------------------- /lib/core/debug/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Debug.Supervisor do 2 | @moduledoc false 3 | 4 | use Supervisor 5 | 6 | ## api 7 | 8 | def start_link, do: :supervisor.start_link(__MODULE__, nil) 9 | 10 | ## :supervisor api 11 | 12 | def init(nil) do 13 | Core.Debug.ensure_table() 14 | supervise([], strategy: :one_for_one) 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/core/register.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Register do 2 | @moduledoc """ 3 | Behaviour for custom process registers. 4 | 5 | `Core` (and `Core.Sys`) can make use of custom registers by using names of the 6 | form `{ :via, module, name }`. `:gen_server`, `supervisor`, `:gen_event`, 7 | `gen_fsm`, `:sys` and other modules from Erlang/OTP can use the custom 8 | register in the same way. For an example of a custom register see `:global` 9 | from Erlang/OTP. 10 | 11 | Note: `whereis_name/1` should return `:undefined`, and not `nil`, when there 12 | is no process associated with a name for compatibility with erlang libraries. 13 | """ 14 | 15 | use Behaviour 16 | 17 | defcallback register_name(any, pid) :: :yes | :no 18 | 19 | defcallback whereis_name(any) :: pid | :undefined 20 | 21 | defcallback unregister_name(any) :: any 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/core/sys.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Sys do 2 | @moduledoc """ 3 | ## Examples 4 | 5 | defmodule PingPong do 6 | 7 | use Core.Sys 8 | 9 | @spec ping(Core.t) :: :pong 10 | def ping(process), do: Core.call(process, __MODULE__, :ping, 5000) 11 | 12 | @spec count(Core.t) :: non_neg_integer 13 | def count(process), do: Core.call(process, __MODULE__, :count, 5000) 14 | 15 | @spec close(Core.t) :: :ok 16 | def close(process), do: Core.call(process, __MODULE__, :close, 5000) 17 | 18 | # die/1 will print alot of information because the exit reason is abnormal. 19 | @spec die(Core.t) :: :ok 20 | def die(process), do: Core.call(process, __MODULE__, :die, 5000) 21 | 22 | @spec start_link() :: { :ok, pid } 23 | def start_link() do 24 | Core.start_link(nil, __MODULE__, nil, 25 | [{ :debug, [{ :log, 10 }, { :stats, true }] }]) 26 | end 27 | 28 | ## Core api 29 | 30 | def init(parent, debug, _args) do 31 | Core.init_ack() 32 | loop(0, parent, debug) 33 | end 34 | 35 | ## Core.Sys (minimal) api 36 | 37 | def system_continue(count, parent, debug), do: loop(count, parent, debug) 38 | 39 | def system_terminate(count, parent, debug, reason) do 40 | terminate(count, parent, debug, reason) 41 | end 42 | 43 | ## Internal 44 | 45 | defp loop(count, parent, debug) do 46 | Core.Sys.receive(__MODULE__, count, parent, debug) do 47 | { __MODULE__, from, :ping } -> 48 | # It is not required to record events using `Core.Debug.event/1` but is 49 | # a useful debug feature that is compiled to a no-op in production. 50 | debug = Core.Debug.event(debug, { :in, :ping, elem(from, 0) }) 51 | Core.reply(from, :pong) 52 | debug = Core.Debug.event(debug, { :out, :pong, elem(from, 0) }) 53 | count = count + 1 54 | debug = Core.Debug.event(debug, { :count, count }) 55 | loop(count, parent, debug) 56 | { __MODULE__, from, :count } -> 57 | debug = Core.Debug.event(debug, { :in, :count, elem(from, 0) }) 58 | Core.reply(from, count) 59 | debug = Core.Debug.event(debug, { :out, count, elem(from, 0) }) 60 | loop(count, parent, debug) 61 | { __MODULE__, from, :close } -> 62 | debug = Core.Debug.event(debug, { :in, :close, elem(from, 0) }) 63 | Core.reply(from, :ok) 64 | debug = Core.Debug.event(debug, { :out, :ok, elem(from, 0) }) 65 | terminate(count, parent, debug, :normal) 66 | { __MODULE__, from, :die } -> 67 | debug = Core.Debug.event(debug, { :in, :die, elem(from, 0) }) 68 | Core.reply(from, :ok) 69 | debug = Core.Debug.event(debug, { :out, :ok, elem(from, 0) }) 70 | terminate(count, parent, debug, :die) 71 | end 72 | end 73 | 74 | defp terminate(count, parent, debug, reason) do 75 | event = { :EXIT, reason } 76 | debug = Core.Debug.event(debug, event) 77 | Core.stop(__MODULE__, count, parent, debug, reason, event) 78 | end 79 | 80 | end 81 | 82 | """ 83 | 84 | use Behaviour 85 | 86 | @doc """ 87 | Returns the state of the process. 88 | """ 89 | defcallback system_get_state(data) :: state 90 | 91 | @doc """ 92 | Updates the state of the process. 93 | """ 94 | defcallback system_update_state(data, update) :: { state, data } 95 | 96 | @doc """ 97 | Returns all data about the process. 98 | """ 99 | defcallback system_get_data(data) :: data 100 | 101 | @doc """ 102 | Changes the data for use with a different version of a module. 103 | """ 104 | defcallback system_change_data(data, module, vsn, extra) :: data 105 | 106 | @doc """ 107 | Called when the process should re-enter it's loop after handling 108 | system messages. 109 | """ 110 | defcallback system_continue(data, Core.parent, Core.Debug.t) :: no_return 111 | 112 | @doc """ 113 | Called when the process should terminate after handling system 114 | messages, i.e. an exit signal was received from the parent process 115 | when the current process is trapping exits. 116 | """ 117 | defcallback system_terminate(data, Core.parent, Core.Debug.t, any) :: 118 | no_return 119 | 120 | ## types 121 | 122 | @typep data :: any 123 | @typep state :: any 124 | @typep update :: ((data) -> { state, data }) 125 | @typep vsn :: any 126 | @typep extra :: any 127 | @type status :: map 128 | 129 | ## exceptions 130 | 131 | defmodule CallbackError do 132 | 133 | defexception [action: nil, kind: nil, payload: nil, stacktrace: [], 134 | message: "callback failed"] 135 | 136 | def exception(opts) do 137 | action = opts[:action] 138 | kind = opts[:kind] 139 | payload = opts[:payload] 140 | stacktrace = Keyword.get(opts, :stacktrace, []) 141 | message = "failure in " <> format_action(action) <> "\n" <> 142 | format_reason(kind, payload, stacktrace) 143 | %Core.Sys.CallbackError{action: action, kind: kind, payload: payload, 144 | stacktrace: stacktrace, message: message} 145 | end 146 | 147 | defp format_action(nil), do: "unknown function" 148 | defp format_action(fun), do: inspect(fun) 149 | 150 | defp format_reason(kind, payload, stacktrace) do 151 | reason = Exception.format(kind, payload, stacktrace) 152 | " " <> Regex.replace(~r/\n/, reason, "\n ") 153 | end 154 | 155 | end 156 | 157 | defmodule CatchLevelError do 158 | 159 | defexception [level: nil, message: "catch level error"] 160 | 161 | def exception(opts) do 162 | level = Keyword.fetch!(opts, :level) 163 | %Core.Sys.CatchLevelError{level: level, message: format_message(level)} 164 | end 165 | 166 | defp format_message(0) do 167 | "process was not started by Core or did not use Core to hibernate" 168 | end 169 | # 3 levels is one extra catch 170 | defp format_message(level) when level >= 3 do 171 | "Core.Sys loop was not entered by a tail call, #{level-2} catch(es)" 172 | end 173 | end 174 | 175 | ## macros 176 | 177 | @doc """ 178 | Macro to handle system messages but not receive any other messages. 179 | 180 | `mod.system_continue/3` or `mod.system_terminate/4` will return 181 | control of the process to `mod` after handling system messages. 182 | 183 | Should only be used as a tail call as it has no local return. 184 | """ 185 | defmacro receive(mod, data, parent, debug) do 186 | parent_quote = quote do: parent_var 187 | extra = transform_receive(mod, data, parent_quote, debug) 188 | quote do 189 | parent_var = unquote(parent) 190 | receive do: unquote(extra) 191 | end 192 | end 193 | 194 | @doc """ 195 | Macro to handle system messages and receive messages. 196 | 197 | `mod.system_continue/3` or `mod.system_terminate/4` will return 198 | control of the process to `mod` after handling system messages. 199 | 200 | Should only be used as a tail call as it has no local return. 201 | 202 | ## Examples 203 | 204 | use Core.Sys 205 | 206 | defp loop(data, parent, debug) do 207 | Core.Sys.receive(__MODULE__, data, parent, debug) do 208 | msg -> 209 | IO.puts(:stdio, inspect(msg)) 210 | loop(data, parent, debug) 211 | after 212 | 1000 -> 213 | terminate(data, parent, debug, timeout) 214 | end 215 | end 216 | 217 | def system_continue(data, parent, debug) do 218 | loop(data, parent, debug) 219 | end 220 | 221 | def system_terminate(_data, _parent, _debug, reason) do 222 | terminate(reason) 223 | end 224 | 225 | defp terminate(reason) do 226 | exit(reaosn) 227 | end 228 | 229 | """ 230 | defmacro receive(mod, data, parent, debug, do: do_clauses) do 231 | parent_quote = quote do: parent_var 232 | extra = transform_receive(mod, data, parent_quote, debug) 233 | quote do 234 | parent_var = unquote(parent) 235 | receive do: unquote(extra ++ do_clauses) 236 | end 237 | end 238 | 239 | defmacro receive(mod, data, parent, debug, do: do_clauses, 240 | after: after_clause) do 241 | parent_quote = quote do: parent_var 242 | extra = transform_receive(mod, data, parent_quote, debug) 243 | quote do 244 | parent_var = unquote(parent) 245 | receive do: unquote(extra ++ do_clauses), after: unquote(after_clause) 246 | end 247 | end 248 | 249 | defmacro __using__(_options) do 250 | quote location: :keep do 251 | 252 | use Core 253 | @behaviour Core.Sys 254 | require Core.Sys 255 | 256 | @doc false 257 | def system_get_state(data), do: data 258 | 259 | def system_update_state(data, update) do 260 | data = update.(data) 261 | { data, data } 262 | end 263 | 264 | def system_get_data(data), do: data 265 | 266 | def system_change_data(data, _module, _vsn, _extra), do: data 267 | 268 | defoverridable [system_get_state: 1, system_update_state: 2, 269 | system_get_data: 1, system_change_data: 4] 270 | 271 | end 272 | end 273 | 274 | defmacrop default_timeout(), do: 5000 275 | 276 | ## debug api 277 | 278 | @doc """ 279 | Pings an OTP process to see if it is responsive to system messages. 280 | Returns `:pong` on success. 281 | """ 282 | @spec ping(Core.t, timeout) :: :pong 283 | def ping(process, timeout \\ default_timeout()) do 284 | { :error, { :unknown_system_msg, :ping } } = call(process, :ping, timeout) 285 | :pong 286 | end 287 | 288 | @doc """ 289 | Suspends an OTP process so that it can only handle system messages. 290 | """ 291 | @spec suspend(Core.t, timeout) :: :ok 292 | def suspend(process, timeout \\ default_timeout()) do 293 | call(process, :suspend, timeout) 294 | end 295 | 296 | @doc """ 297 | Resumes an OTP process that has been system suspended. 298 | """ 299 | @spec resume(Core.t, timeout) :: :ok 300 | def resume(process, timeout \\ default_timeout()) do 301 | call(process, :resume, timeout) 302 | end 303 | 304 | @doc """ 305 | Returns the state of an OTP process. 306 | """ 307 | @spec get_state(Core.t, timeout) :: state 308 | def get_state(process, timeout \\ default_timeout()) do 309 | state_call(process, :get_state, timeout) 310 | end 311 | 312 | @doc """ 313 | Sets the state of an OTP process. 314 | """ 315 | @spec set_state(Core.t, state, timeout) :: :ok 316 | def set_state(process, state, timeout \\ default_timeout()) do 317 | update = fn(_old_state) -> state end 318 | ^state = state_call(process, { :replace_state, update }, timeout) 319 | :ok 320 | end 321 | 322 | @doc """ 323 | Updates the state of an OTP process. 324 | Returns the updated state. 325 | """ 326 | @spec update_state(Core.t, update, timeout) :: state 327 | def update_state(process, update, timeout \\ default_timeout()) do 328 | state_call(process, { :replace_state, update }, timeout) 329 | end 330 | 331 | @doc """ 332 | Returns status information about an OTP process. 333 | 334 | This function will return alot of information, including the process 335 | dictionary of the target process. 336 | """ 337 | @spec get_status(Core.t, timeout) :: status 338 | def get_status(process, timeout \\ default_timeout()) do 339 | call(process, :get_status, timeout) 340 | |> parse_status() 341 | end 342 | 343 | @doc """ 344 | Returns the data held about an OTP process. 345 | 346 | This function will return any data held about a process. In many cases 347 | this will return the same as `get_state/2`. 348 | """ 349 | @spec get_data(Core.t, timeout) :: data | nil 350 | def get_data(process, timeout \\ default_timeout()) do 351 | get_status(process, timeout) 352 | |> Dict.get(:data) 353 | end 354 | 355 | @doc """ 356 | Change the data of an OTP process due to a module version change. 357 | 358 | Can only be used on system suspended processes. 359 | """ 360 | @spec change_data(Core.t, module, vsn, extra, timeout) :: :ok 361 | def change_data(process, mod, oldvsn, extra, timeout \\ default_timeout()) do 362 | case state_call(process, { :change_code, mod, oldvsn, extra }, timeout) do 363 | :ok -> 364 | :ok 365 | { :error, {:EXIT, reason } } -> 366 | raise Core.Sys.CallbackError, [kind: :exit, payload: reason] 367 | # direct :sys module did not return { :ok, data } 368 | { :error, other } -> 369 | raise Core.Sys.CallbackError, 370 | [kind: :error, payload: other] 371 | end 372 | end 373 | 374 | @doc """ 375 | Returns any logged events created by an OTP process. 376 | 377 | The oldest event is at the head of the list. 378 | """ 379 | @spec get_log(Core.t, timeout) :: [Core.Debug.event] 380 | def get_log(process, timeout \\ default_timeout()) do 381 | debug_call(process, { :log, :get }, timeout) 382 | |> Core.Debug.log_from_raw() 383 | end 384 | 385 | @doc """ 386 | Prints any logged events created by an OTP process to `:stdio`. 387 | """ 388 | @spec print_log(Core.t, timeout) :: :ok 389 | def print_log(process, timeout \\ default_timeout()) do 390 | raw_log = debug_call(process, { :log, :get }, timeout) 391 | Core.Debug.write_raw_log(:stdio, process, raw_log) 392 | end 393 | 394 | @doc """ 395 | Sets the number of logged events to store for an OTP process. 396 | """ 397 | @spec set_log(Core.t, non_neg_integer | boolean, timeout) :: :ok 398 | def set_log(process, max, timeout \\ default_timeout()) 399 | 400 | def set_log(process, 0, timeout) do 401 | debug_call(process, { :log, false }, timeout) 402 | end 403 | 404 | def set_log(process, max, timeout) when is_integer(max) and max > 0 do 405 | debug_call(process, { :log, { :true, max }}, timeout) 406 | end 407 | 408 | @doc """ 409 | Set the file to log events to for an OTP process. 410 | `nil` will turn off logging events to file. 411 | """ 412 | @spec set_log_file(Core.t, Path.t | nil, timeout) :: :ok 413 | def set_log_file(process, path, timeout \\ default_timeout()) 414 | 415 | def set_log_file(process, nil, timeout) do 416 | debug_call(process, { :log_to_file, false }, timeout) 417 | end 418 | 419 | def set_log_file(process, path, timeout) do 420 | case debug_call(process, { :log_to_file, path }, timeout) do 421 | :ok -> 422 | :ok 423 | { :error, :open_file } -> 424 | raise ArgumentError, message: "could not open file: #{inspect(path)}" 425 | end 426 | end 427 | 428 | @doc """ 429 | Returns stats about an OTP process if it is collecting statistics. 430 | Otherwise returns `nil`. 431 | """ 432 | @spec get_stats(Core.t, timeout) :: Core.Debug.stats | nil 433 | def get_stats(process, timeout \\ default_timeout()) do 434 | debug_call(process, { :statistics, :get }, timeout) 435 | |> Core.Debug.stats_from_raw() 436 | end 437 | 438 | @doc """ 439 | Prints statistics collected by an OTP process to `:stdio`. 440 | """ 441 | @spec print_stats(Core.t, timeout) :: :ok 442 | def print_stats(process, timeout \\ default_timeout()) do 443 | stats = get_stats(process, timeout) 444 | Core.Debug.write_stats(:stdio, process, stats) 445 | end 446 | 447 | @doc """ 448 | Sets whether an OTP process collects statistics or not. 449 | `true` will collect statistics. 450 | `false` will not collect statistics. 451 | """ 452 | @spec set_stats(Core.t, boolean, timeout) :: :ok 453 | def set_stats(process, flag, timeout \\ default_timeout()) 454 | when is_boolean(flag) do 455 | debug_call(process, { :statistics, flag }, timeout) 456 | end 457 | 458 | @doc """ 459 | Sets whether an OTP process should print events to `:stdio` as they occur. 460 | `true` will print events. 461 | `false` will not print events. 462 | """ 463 | @spec set_trace(Core.t, boolean, timeout) :: :ok 464 | def set_trace(process, flag, timeout \\ default_timeout()) 465 | when is_boolean(flag) do 466 | debug_call(process, { :trace, flag }, timeout) 467 | end 468 | 469 | @doc """ 470 | Sets a hook to act on events for an OTP process. 471 | Setting the hook state to `nil` will remove the hook. 472 | """ 473 | @spec set_hook(Core.t, Core.Debug.hook, Core.Debug.hook_state | nil, 474 | timeout) :: :ok 475 | def set_hook(process, hook, state, timeout \\ default_timeout()) 476 | 477 | def set_hook(process, hook, nil, timeout) when is_function(hook, 3) do 478 | debug_call(process, { :remove, hook }, timeout) 479 | end 480 | 481 | def set_hook(process, hook, state, timeout) when is_function(hook, 3) do 482 | debug_call(process, { :install, { hook, state } }, timeout) 483 | end 484 | 485 | ## receive macro api 486 | 487 | @doc false 488 | @spec message(__MODULE__, data, Core.parent, Core.Debug.t, any, Core.from) :: 489 | no_return 490 | def message(mod, data, parent, debug, msg, from) do 491 | :sys.handle_system_msg(msg, from, parent, __MODULE__, debug, [mod | data]) 492 | end 493 | 494 | ## :sys api 495 | 496 | def system_continue(parent, debug, [mod | data]) do 497 | continue(mod, :system_continue, data, parent, debug) 498 | end 499 | 500 | @doc false 501 | def system_terminate(reason, parent, _debug, [mod | data]) do 502 | continue(mod, :system_terminate, data, parent, [reason]) 503 | end 504 | 505 | @doc false 506 | def system_get_state([mod | data]) do 507 | try do 508 | mod.system_get_state(data) 509 | else 510 | state -> 511 | { :ok, state } 512 | rescue 513 | # Callback failed in a callback of mod 514 | exception in [Core.Sys.CallbackError] -> 515 | raise exception 516 | catch 517 | kind, payload -> 518 | raise Core.Sys.CallbackError, 519 | action: :erlang.make_fun(mod, :system_get_state, 1), 520 | kind: kind, payload: payload, stacktrace: System.stacktrace() 521 | end 522 | end 523 | 524 | @doc false 525 | def system_replace_state(replace, [mod | data]) do 526 | try do 527 | { _state, _data} = mod.system_update_state(data, replace) 528 | else 529 | { state, data } -> 530 | { :ok, state, [mod | data] } 531 | rescue 532 | # Callback failed in a callback of mod 533 | exception in [Core.Sys.CallbackError] -> 534 | raise exception 535 | catch 536 | kind, payload -> 537 | raise Core.Sys.CallbackError, 538 | action: :erlang.make_fun(mod, :system_update_state, 2), 539 | kind: kind, payload: payload, stacktrace: System.stacktrace() 540 | end 541 | end 542 | 543 | @doc false 544 | def format_status(_type, [_pdict, sys_status, parent, debug, [mod | data]]) do 545 | base_status = format_base_status(sys_status, parent, mod, debug) 546 | try do 547 | mod.system_get_data(data) 548 | else 549 | mod_data -> 550 | base_status ++ [{ :data, [{ 'Module data', mod_data }] }] 551 | rescue 552 | exception in [Core.Sys.CallbackError] -> 553 | format_status_error(base_status, data, exception) 554 | catch 555 | kind, payload -> 556 | exception = Core.Sys.CallbackError.exception([ 557 | action: :erlang.make_fun(mod, :system_get_data, 1), 558 | kind: kind, payload: payload, stacktrace: System.stacktrace()]) 559 | format_status_error(base_status, data, exception) 560 | end 561 | end 562 | 563 | @doc false 564 | def system_code_change([mod | data], change_mod, oldvsn, extra) do 565 | try do 566 | mod.system_change_data(data, change_mod, oldvsn, extra) 567 | else 568 | data -> 569 | { :ok, [mod | data] } 570 | rescue 571 | # Callback failed in a callback of mod. 572 | exception in [Core.Sys.CallbackError] -> 573 | code_change_error(exception) 574 | catch 575 | kind, payload -> 576 | exception = Core.Sys.CallbackError.exception([ 577 | action: :erlang.make_fun(mod, :system_change_data, 4), 578 | kind: kind, payload: payload, stacktrace: System.stacktrace()]) 579 | code_change_error(exception) 580 | end 581 | end 582 | 583 | ## internal 584 | 585 | ## receive 586 | 587 | defp transform_receive(mod, data, parent, debug) do 588 | quote do 589 | { :EXIT, ^unquote(parent), reason } -> 590 | unquote(mod).system_terminate(unquote(data), unquote(parent), 591 | unquote(debug), reason) 592 | { :system, from, msg } -> 593 | Core.Sys.message(unquote(mod), unquote(data), unquote(parent), 594 | unquote(debug), msg, from) 595 | end 596 | end 597 | 598 | ## calls 599 | 600 | defp call(process, request, timeout) do 601 | Core.call(process, :system, request, timeout) 602 | end 603 | 604 | defp state_call(process, request, timeout) do 605 | case call(process, request, timeout) do 606 | { :ok, result } -> 607 | result 608 | { :error, { :unknown_system_msg, { :change_code, _, _, _ } } } -> 609 | raise ArgumentError, message: "#{Core.format(process)} is running" 610 | { :error, { :unknown_system_msg, _request } } -> 611 | raise ArgumentError, message: "#{Core.format(process)} is suspended" 612 | :ok -> 613 | :ok 614 | # exception raised by this module, raise it. 615 | { :error, { :callback_failed, { __MODULE__, _ }, 616 | { :error, %Core.Sys.CallbackError{} = exception } } } -> 617 | raise exception 618 | # raise in a direct :sys module 619 | { :error, { :callback_failed, action, { kind, payload } } } -> 620 | raise Core.Sys.CallbackError, action: action, kind: kind, 621 | payload: payload 622 | state -> 623 | state 624 | end 625 | end 626 | 627 | defp debug_call(process, request, timeout) do 628 | case call(process, { :debug, request }, timeout) do 629 | :ok -> 630 | :ok 631 | { :ok, result } -> 632 | result 633 | { :error, _reason } = error -> 634 | error 635 | end 636 | end 637 | 638 | ## :sys api 639 | 640 | defp parse_status({ :status, pid, { :module, __MODULE__ }, 641 | [pdict, sys_status, parent, _debug, status]}) do 642 | status_data = get_status_data(status) 643 | case get(status_data, 'Module error') do 644 | nil -> 645 | parse_status_data(pid, __MODULE__, pdict, sys_status, parent, 646 | status_data) 647 | { :callback_failed, { __MODULE__, _ }, { :error, exception } } -> 648 | raise exception 649 | end 650 | end 651 | 652 | defp parse_status({ :status, pid, { :module, mod }, 653 | [pdict, sys_status, parent, _debug, status]}) do 654 | status_data = get_status_data(status) 655 | parse_status_data(pid, mod, pdict, sys_status, parent, status_data) 656 | end 657 | 658 | defp get_status_data(status) do 659 | try do 660 | data = Keyword.get_values(status, :data) 661 | # special case for :gen_event (or similar direct :sys) 662 | items = Keyword.get_values(status, :items) 663 | List.flatten([items, data]) 664 | rescue 665 | # not formed as expected, pass whole status as state. Will be used 666 | # as data term later. 667 | exception -> 668 | IO.puts(:user, inspect(exception)) 669 | [{ 'State', status }] 670 | end 671 | end 672 | 673 | defp parse_status_data(pid, mod, pdict, sys_status, parent, status_data) do 674 | name = get(status_data, 'Name', pid) 675 | log = parse_status_log(status_data) 676 | stats = parse_status_stats(status_data) 677 | mod2 = get(status_data, 'Module', mod) 678 | mod_data = get_mod_data(mod, status_data) 679 | %{ name: name, log: log, stats: stats, module: mod2, data: mod_data, 680 | pid: pid, dictionary: pdict, sys_status: sys_status, parent: parent } 681 | end 682 | 683 | defp parse_status_log(status_data) do 684 | case get(status_data, 'Logged events', []) do 685 | [] -> 686 | [] 687 | { _max, rev_raw_log } -> 688 | Enum.reverse(rev_raw_log) 689 | |> Core.Debug.log_from_raw() 690 | end 691 | end 692 | 693 | defp parse_status_stats(status_data) do 694 | get(status_data, 'Statistics', :no_statistics) 695 | |> Core.Debug.stats_from_raw() 696 | end 697 | 698 | defp get_mod_data(__MODULE__, status_data) do 699 | get(status_data, 'Module data') 700 | end 701 | 702 | defp get_mod_data(:gen_fsm, status_data) do 703 | state_name = get(status_data, 'StateName') 704 | state_data = get(status_data, 'StateData') 705 | { state_name, state_data } 706 | end 707 | 708 | defp get_mod_data(:gen_event, status_data) do 709 | to_data = fn({ :handler, mod, id, state, _sup }) -> { mod, id, state} end 710 | get(status_data, 'Installed handlers') 711 | |> Enum.map(to_data) 712 | end 713 | 714 | defp get_mod_data(_mod, status_data) do 715 | get(status_data, 'State', status_data) 716 | end 717 | 718 | defp format_base_status(sys_status, parent, mod, debug) do 719 | header = String.to_char_list("Status for #{inspect(mod)} #{Core.format()}") 720 | log = get_status_log(debug) 721 | stats = get_status_stats(debug) 722 | data = [{ 'Status', sys_status }, { 'Parent', parent }, 723 | { 'Name', Core.get_name() }, { 'Logged events', log }, 724 | { 'Statistics', stats }, { 'Module', mod }] 725 | [{ :header, header }, { :data, data }] 726 | end 727 | 728 | defp get_status_log(debug) do 729 | try do 730 | # How gen_* returns format_status log 731 | :sys.get_debug(:log, debug, []) 732 | rescue 733 | _exception -> 734 | [] 735 | end 736 | end 737 | 738 | defp get_status_stats(debug) do 739 | try do 740 | Core.Debug.get_raw_stats(debug) 741 | rescue 742 | _exception -> 743 | :no_statistics 744 | end 745 | end 746 | 747 | defp format_status_error(base_status, mod_data, exception) do 748 | reason = { :callback_failed, { __MODULE__, :format_status }, 749 | { :error, exception } } 750 | mod_status = [{ :data, 751 | [{ 'Module data', mod_data }, { 'Module error', reason }] }] 752 | base_status ++ mod_status 753 | end 754 | 755 | defp code_change_error(exception) do 756 | # Does not match { :ok, data } causing { :error, { :callback_failed, ..}} to 757 | # be returned by :sys. This is the same form as the replace/get_state error 758 | # message returned in 17.0 final. Therefore even though it is handled 759 | # differently in :sys, to Core.Sys modules it will appear to be handled the 760 | # same as replace/get_state. 761 | { :callback_failed, { __MODULE__, :system_code_change }, 762 | { :error, exception } } 763 | end 764 | 765 | defp continue(mod, fun, data, parent, debug, args \\ []) do 766 | case Process.info(self(), :catchlevel) do 767 | # Assume :proc_lib catch but no Core catch due to hibernation. 768 | { :catchlevel, 1 } -> 769 | # Use Core to re-add Core catch for logging exceptions. 770 | Core.continue(mod, fun, data, parent, debug, args) 771 | # Assume :proc_lib catch and Core catch, i.e. no hibernation. 772 | { :catchlevel, 2 } -> 773 | apply(mod, fun, [data, parent, debug] ++ args) 774 | # Either not a Core/:proc_lib process OR 775 | # Core.Sys.receive/4 macro was used inside a catch. 776 | { :catchlevel, level } -> 777 | try do 778 | raise Core.Sys.CatchLevelError, level: level 779 | rescue 780 | exception -> 781 | # Hibernation didn't occur (level would be 1) so generate a 782 | # stacktrace (and exception), which hopefully will be logged by mod. 783 | reason = { exception, System.stacktrace()} 784 | # Use Core to add Core catch which may log an exception if one is 785 | # raised. If we have two levels (or more!) of the Core catch an 786 | # exception will only be logged once. 787 | Core.continue(mod, :system_terminate, data, parent, debug, 788 | [reason]) 789 | end 790 | end 791 | end 792 | 793 | ## util 794 | 795 | defp get(list, key, default \\ nil) do 796 | case List.keyfind(list, key, 0, nil) do 797 | nil -> 798 | default 799 | { _key, value } -> 800 | value 801 | end 802 | end 803 | 804 | end 805 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Core.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ app: :core, 6 | version: "0.14.1", 7 | elixir: "~> 0.14.1 or ~> 0.15.0 or ~> 1.0.0 or ~> 1.1.0-dev", 8 | description: "Library for selective receive OTP processes", 9 | package: package(), 10 | deps: deps() ] 11 | end 12 | 13 | def application do 14 | [ applications: [], 15 | mod: { Core.App, [] }] 16 | end 17 | 18 | defp package() do 19 | [files: ["lib", "mix.exs", "README.md", "LICENSE"], 20 | contributors: ["James Fish"], 21 | licenses: ["Apache 2.0"], 22 | links: [{ "Github", "https://github.com/fishcakez/core" }] ] 23 | end 24 | 25 | defp deps() do 26 | [{ :ex_doc, ">= 0.5.2", only: [:docs]}] 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /test/core/debug_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule Core.DebugTest do 4 | use ExUnit.Case 5 | 6 | require Core.Debug 7 | 8 | setup_all do 9 | TestIO.setup_all() 10 | end 11 | 12 | setup do 13 | TestIO.setup() 14 | :ok 15 | end 16 | 17 | test "get/set default options" do 18 | assert Core.Debug.set_opts([{ :log, 10 }, { :stats, true }]) === :ok 19 | assert [{ :log, 10}, {:stats, true }] == Core.Debug.get_opts() 20 | assert Core.Debug.set_opts([{ :log, 10 }]) === :ok 21 | assert [{ :log, 10 }] = Core.Debug.get_opts() 22 | assert Core.Debug.set_opts([]) === :ok 23 | end 24 | 25 | test "log with 2 events and get log" do 26 | debug = Core.Debug.new([{ :log, 10 }]) 27 | debug = Core.Debug.event(debug, { :event, 1 }) 28 | debug = Core.Debug.event(debug, { :event, 2 }) 29 | # order is important 30 | assert [{ :event, 1 }, { :event, 2 }] = Core.Debug.get_log(debug) 31 | end 32 | 33 | test "log with 0 events and get log" do 34 | debug = Core.Debug.new([{ :log, 10 }]) 35 | assert Core.Debug.get_log(debug) === [] 36 | end 37 | 38 | test "no log with 2 events and get log" do 39 | debug = Core.Debug.new([]) 40 | debug = Core.Debug.event(debug, { :event, 1 }) 41 | debug = Core.Debug.event(debug, { :event, 2 }) 42 | assert Core.Debug.get_log(debug) === [] 43 | end 44 | 45 | test "log with 2 events and print log" do 46 | debug = Core.Debug.new([{ :log, 10 }]) 47 | debug = Core.Debug.event(debug, { :event, 1 }) 48 | debug = Core.Debug.event(debug, { :event, 2 }) 49 | assert Core.Debug.print_log(debug) === :ok 50 | report = "** Core.Debug #{inspect(self())} event log:\n" <> 51 | "** Core.Debug #{inspect(self())} #{inspect({ :event, 1 })}\n" <> 52 | "** Core.Debug #{inspect(self())} #{inspect({ :event, 2 })}\n" <> 53 | "\n" 54 | assert TestIO.binread() === report 55 | end 56 | 57 | test "log with 0 events and print log" do 58 | debug = Core.Debug.new([{ :log, 10 }]) 59 | assert Core.Debug.print_log(debug) === :ok 60 | report = "** Core.Debug #{inspect(self())} event log is empty\n" <> 61 | "\n" 62 | assert TestIO.binread() === report 63 | end 64 | 65 | test "no log with 2 events and print log" do 66 | debug = Core.Debug.new([]) 67 | debug = Core.Debug.event(debug, { :event, 1 }) 68 | debug = Core.Debug.event(debug, { :event, 2 }) 69 | assert Core.Debug.print_log(debug) === :ok 70 | report = "** Core.Debug #{inspect(self())} event log is empty\n\n" 71 | assert TestIO.binread() === report 72 | end 73 | 74 | test "log with cast message in and print log" do 75 | debug = Core.Debug.new([{ :log, 10 }]) 76 | debug = Core.Debug.event(debug, { :in, :hello }) 77 | assert Core.Debug.print_log(debug) === :ok 78 | report = "** Core.Debug #{inspect(self())} event log:\n" <> 79 | "** Core.Debug #{inspect(self())} message in: :hello\n" <> 80 | "\n" 81 | assert TestIO.binread() === report 82 | end 83 | 84 | test "log with call message in and print log" do 85 | debug = Core.Debug.new([{ :log, 10 }]) 86 | pid = spawn(fn() -> :ok end) 87 | debug = Core.Debug.event(debug, { :in, :hello, pid }) 88 | assert Core.Debug.print_log(debug) === :ok 89 | report = "** Core.Debug #{inspect(self())} event log:\n" <> 90 | "** Core.Debug #{inspect(self())} " <> 91 | "message in (from #{inspect(pid)}): :hello\n" <> 92 | "\n" 93 | assert TestIO.binread() === report 94 | end 95 | 96 | test "log with message out and print log" do 97 | debug = Core.Debug.new([{ :log, 10 }]) 98 | pid = spawn(fn() -> :ok end) 99 | debug = Core.Debug.event(debug, { :out, :hello, pid }) 100 | assert Core.Debug.print_log(debug) === :ok 101 | report = "** Core.Debug #{inspect(self())} event log:\n" <> 102 | "** Core.Debug #{inspect(self())} " <> 103 | "message out (to #{inspect(pid)}): :hello\n" <> 104 | "\n" 105 | assert TestIO.binread() === report 106 | end 107 | 108 | test "stats with 0 events and get stats" do 109 | min_start = seconds() 110 | start_reductions = reductions() 111 | debug = Core.Debug.new([{ :stats, true }]) 112 | stats = Core.Debug.get_stats(debug) 113 | max_reductions = reductions() - start_reductions 114 | max_current = seconds() 115 | assert stats[:in] === 0 116 | assert stats[:out] === 0 117 | assert stats[:reductions] < max_reductions 118 | assert seconds(stats[:start_time]) >= min_start 119 | assert seconds(stats[:current_time]) <= max_current 120 | assert Map.size(stats) === 5 121 | end 122 | 123 | test "stats with cast message in and get stats" do 124 | debug = Core.Debug.new([{ :stats, true }]) 125 | debug = Core.Debug.event(debug, { :in, :hello }) 126 | stats = Core.Debug.get_stats(debug) 127 | assert stats[:in] === 1 128 | end 129 | 130 | test "stats with call message in and get stats" do 131 | debug = Core.Debug.new([{ :stats, true }]) 132 | debug = Core.Debug.event(debug, { :in, :hello, self() }) 133 | stats = Core.Debug.get_stats(debug) 134 | assert stats[:in] === 1 135 | end 136 | 137 | test "stats with message out and get stats" do 138 | debug = Core.Debug.new([{ :stats, true }]) 139 | debug = Core.Debug.event(debug, { :out, :hello, self() }) 140 | stats = Core.Debug.get_stats(debug) 141 | assert stats[:out] === 1 142 | end 143 | 144 | test "stats with one of each event and print stats" do 145 | debug = Core.Debug.new([{ :stats, true }]) 146 | debug = Core.Debug.event(debug, { :in, :hello, }) 147 | debug = Core.Debug.event(debug, { :in, :hello, self() }) 148 | debug = Core.Debug.event(debug, { :out, :hello, self() }) 149 | assert Core.Debug.print_stats(debug) === :ok 150 | output = TestIO.binread() 151 | pattern = "\\A\\*\\* Core.Debug #{inspect(self())} statistics:\n" <> 152 | " Start Time: \\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\n" <> 153 | " Current Time: \\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\n" <> 154 | " Messages In: 2\n" <> 155 | " Messages Out: 1\n" <> 156 | " Reductions: \\d+\n" <> 157 | "\n\\z" 158 | regex = Regex.compile!(pattern) 159 | assert Regex.match?(regex, output), 160 | "#{inspect(regex)} not found in #{output}" 161 | end 162 | 163 | test "no stats and print stats" do 164 | debug = Core.Debug.new([]) 165 | assert Core.Debug.print_stats(debug) === :ok 166 | report = "** Core.Debug #{inspect(self())} statistics not active\n" <> 167 | "\n" 168 | assert TestIO.binread() === report 169 | end 170 | 171 | 172 | 173 | ## utils 174 | 175 | defp seconds(datetime \\ :erlang.localtime()) do 176 | :calendar.datetime_to_gregorian_seconds(datetime) 177 | end 178 | 179 | defp reductions() do 180 | { :reductions, self_reductions } = Process.info(self(), :reductions) 181 | self_reductions 182 | end 183 | 184 | end 185 | -------------------------------------------------------------------------------- /test/core/sys_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule Core.SysTest do 4 | use ExUnit.Case 5 | 6 | use Core.Sys 7 | 8 | def init(parent, debug, fun) do 9 | Core.init_ack() 10 | loop(fun, parent, debug) 11 | end 12 | 13 | def loop(fun, parent, debug) do 14 | Core.Sys.receive(__MODULE__, fun, parent, debug) do 15 | { __MODULE__, from, { :event, event } } -> 16 | debug = Core.Debug.event(debug, event) 17 | Core.reply(from, :ok) 18 | loop(fun, parent, debug) 19 | { __MODULE__, from, :eval } -> 20 | Core.reply(from, fun.()) 21 | loop(fun, parent, debug) 22 | end 23 | end 24 | 25 | def system_get_state(fun), do: fun.() 26 | 27 | def system_update_state(fun, update) do 28 | fun = update.(fun) 29 | { fun, fun } 30 | end 31 | 32 | def system_get_data(fun), do: fun.() 33 | 34 | def system_change_data(_oldfun, _mod, _oldvsn, newfun), do: newfun.() 35 | 36 | def system_continue(fun, parent, debug), do: loop(fun, parent, debug) 37 | 38 | def system_terminate(_fun, _parent, _debug, reason) do 39 | exit(reason) 40 | end 41 | 42 | setup_all do 43 | logfile = (Path.join(__DIR__, "logfile")) 44 | File.touch(logfile) 45 | on_exit(fn() -> File.rm(Path.join(__DIR__, "logfile")) end) 46 | TestIO.setup_all() 47 | end 48 | 49 | setup do 50 | TestIO.setup() 51 | end 52 | 53 | test "ping" do 54 | pid = Core.spawn_link(__MODULE__, fn() -> nil end) 55 | assert Core.Sys.ping(pid) === :pong 56 | assert close(pid) === :ok 57 | assert TestIO.binread() === <<>> 58 | end 59 | 60 | test "ping :gen_server" do 61 | { :ok, pid } = GS.start_link(fn() -> { :ok, nil } end) 62 | assert Core.Sys.ping(pid) === :pong 63 | assert close(pid) === :ok 64 | assert TestIO.binread() === <<>> 65 | end 66 | 67 | test "ping :gen_event" do 68 | { :ok, pid } = GE.start_link(fn() -> { :ok, nil } end) 69 | assert Core.Sys.ping(pid) === :pong 70 | assert close(pid) === :ok 71 | assert TestIO.binread() === <<>> 72 | end 73 | 74 | test "ping :gen_fsm" do 75 | { :ok, pid} = GFSM.start_link(fn() -> {:ok, :state, nil} end) 76 | assert Core.Sys.ping(pid) === :pong 77 | assert close(pid) === :ok 78 | assert TestIO.binread() === <<>> 79 | end 80 | 81 | test "get_state" do 82 | ref = make_ref() 83 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 84 | assert Core.Sys.get_state(pid) === ref 85 | assert close(pid) === :ok 86 | assert TestIO.binread() === <<>> 87 | end 88 | 89 | test "get_state that raises exception" do 90 | exception = ArgumentError.exception([message: "hello"]) 91 | pid = Core.spawn_link(__MODULE__, fn() -> raise(exception) end) 92 | assert_raise Core.Sys.CallbackError, 93 | ~r"^failure in &Core\.SysTest\.system_get_state/1\n \*\* \(ArgumentError\) hello\n"m, 94 | fn() -> Core.Sys.get_state(pid) end 95 | assert close(pid) === :ok 96 | assert TestIO.binread() === <<>> 97 | end 98 | 99 | test "get_state that throws" do 100 | pid = Core.spawn_link(__MODULE__, fn() -> throw(:hello) end) 101 | assert_raise Core.Sys.CallbackError, 102 | ~r"^failure in &Core\.SysTest\.system_get_state/1\n \*\* \(throw\) :hello\n"m, 103 | fn() -> Core.Sys.get_state(pid) end 104 | assert close(pid) === :ok 105 | assert TestIO.binread() === <<>> 106 | end 107 | 108 | test "get_state that exits" do 109 | pid = Core.spawn_link(__MODULE__, fn() -> exit(:hello) end) 110 | assert_raise Core.Sys.CallbackError, 111 | ~r"^failure in &Core\.SysTest\.system_get_state/1\n \*\* \(exit\) :hello\n"m, 112 | fn() -> Core.Sys.get_state(pid) end 113 | assert close(pid) === :ok 114 | assert TestIO.binread() === <<>> 115 | end 116 | 117 | test "get_state :gen_server" do 118 | ref = make_ref() 119 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 120 | assert Core.Sys.get_state(pid) === ref 121 | assert close(pid) === :ok 122 | assert TestIO.binread() === <<>> 123 | end 124 | 125 | test "get_state :gen_event" do 126 | ref = make_ref() 127 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 128 | assert Core.Sys.get_state(pid) === [{GE, false, ref}] 129 | assert close(pid) === :ok 130 | assert TestIO.binread() === <<>> 131 | end 132 | 133 | test "get_state :gen_fsm" do 134 | ref = make_ref() 135 | { :ok, pid} = GFSM.start_link(fn() -> {:ok, :state, ref} end) 136 | assert Core.Sys.get_state(pid) === { :state, ref } 137 | assert close(pid) === :ok 138 | assert TestIO.binread() === <<>> 139 | end 140 | 141 | test "set_state" do 142 | ref1 = make_ref() 143 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 144 | ref2 = make_ref() 145 | fun = fn() -> ref2 end 146 | assert Core.Sys.set_state(pid, fun) === :ok 147 | assert Core.call(pid, __MODULE__, :eval, 500) === ref2, "state not set" 148 | assert close(pid) === :ok 149 | assert TestIO.binread() === <<>> 150 | end 151 | 152 | test "update_state" do 153 | ref1 = make_ref() 154 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 155 | ref2 = make_ref() 156 | update = fn(fun) -> ( fn() -> { fun.(), ref2 } end ) end 157 | Core.Sys.update_state(pid, update) 158 | assert Core.call(pid, __MODULE__, :eval, 500) === { ref1, ref2 }, 159 | "state not updated" 160 | assert close(pid) === :ok 161 | assert TestIO.binread() === <<>> 162 | end 163 | 164 | test "update_state that raises exception" do 165 | ref = make_ref() 166 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 167 | exception = ArgumentError.exception([message: "hello"]) 168 | update = fn(_fun) -> raise(exception) end 169 | assert_raise Core.Sys.CallbackError, 170 | ~r"^failure in &Core\.SysTest\.system_update_state/2\n \*\* \(ArgumentError\) hello\n"m, 171 | fn() -> Core.Sys.update_state(pid, update) end 172 | assert Core.call(pid, __MODULE__, :eval, 500) === ref, "state changed" 173 | assert close(pid) === :ok 174 | assert TestIO.binread() === <<>> 175 | end 176 | 177 | test "update_state that throws" do 178 | ref = make_ref() 179 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 180 | update = fn(_fun) -> throw(:hello) end 181 | assert_raise Core.Sys.CallbackError, 182 | ~r"^failure in &Core\.SysTest\.system_update_state/2\n \*\* \(throw\) :hello\n"m, 183 | fn() -> Core.Sys.update_state(pid, update) end 184 | assert Core.call(pid, __MODULE__, :eval, 500) === ref, "state changed" 185 | assert close(pid) === :ok 186 | assert TestIO.binread() === <<>> 187 | end 188 | 189 | test "update_state that exits" do 190 | ref = make_ref() 191 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 192 | update = fn(_fun) -> exit(:hello) end 193 | assert_raise Core.Sys.CallbackError, 194 | ~r"^failure in &Core\.SysTest\.system_update_state/2\n \*\* \(exit\) :hello\n"m, 195 | fn() -> Core.Sys.update_state(pid, update) end 196 | assert Core.call(pid, __MODULE__, :eval, 500) === ref, "state changed" 197 | assert close(pid) === :ok 198 | assert TestIO.binread() === <<>> 199 | end 200 | 201 | test "update_state :gen_server" do 202 | ref1 = make_ref() 203 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref1 } end) 204 | ref2 = make_ref() 205 | update = fn(state) -> { state, ref2 } end 206 | assert Core.Sys.update_state(pid, update) === { ref1, ref2 } 207 | assert close(pid) === :ok 208 | assert TestIO.binread() === <<>> 209 | end 210 | 211 | test "update_state :gen_event" do 212 | ref1 = make_ref() 213 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref1 } end) 214 | ref2 = make_ref() 215 | update = fn({mod, id, state}) -> { mod, id, { state, ref2 } } end 216 | assert Core.Sys.update_state(pid, update) === [{GE, false, { ref1, ref2 } }] 217 | assert close(pid) === :ok 218 | assert TestIO.binread() === <<>> 219 | end 220 | 221 | test "update_state :gen_fsm" do 222 | ref1 = make_ref() 223 | { :ok, pid} = GFSM.start_link(fn() -> {:ok, :state, ref1} end) 224 | ref2 = make_ref() 225 | update = fn({ state_name, state_data }) -> 226 | { state_name, { state_data, ref2 } } 227 | end 228 | assert Core.Sys.update_state(pid, update) === { :state, { ref1, ref2 } } 229 | assert close(pid) === :ok 230 | assert TestIO.binread() === <<>> 231 | end 232 | 233 | test "get_status" do 234 | ref = make_ref() 235 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 236 | status = Core.Sys.get_status(pid) 237 | assert status[:module] === __MODULE__ 238 | assert status[:data] === ref 239 | assert status[:pid] === pid 240 | assert status[:name] === pid 241 | assert status[:log] === [] 242 | assert status[:stats] === nil 243 | assert status[:sys_status] === :running 244 | assert status[:parent] === self() 245 | assert Map.has_key?(status, :dictionary) 246 | assert close(pid) === :ok 247 | assert TestIO.binread() === <<>> 248 | end 249 | 250 | test "get_status with log and 2 events" do 251 | ref = make_ref() 252 | pid = Core.spawn_link(__MODULE__, fn() -> ref end, 253 | [{ :debug, [{ :log, 10 }] }]) 254 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 255 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 256 | status = Core.Sys.get_status(pid) 257 | assert status[:log] === [ { :event, 1}, { :event, 2} ] 258 | assert close(pid) === :ok 259 | assert TestIO.binread() === <<>> 260 | end 261 | 262 | test "get_status with stats and 1 cast message" do 263 | ref = make_ref() 264 | pid = Core.spawn_link(__MODULE__, fn() -> ref end, 265 | [{ :debug, [{ :stats, true }] }]) 266 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello } }, 500) 267 | status = Core.Sys.get_status(pid) 268 | stats = status[:stats] 269 | assert is_map(stats), "stats not returned" 270 | assert stats[:in] === 1 271 | assert stats[:out] === 0 272 | assert is_integer(stats[:reductions]) 273 | assert stats[:start_time] <= stats[:current_time] 274 | assert close(pid) === :ok 275 | assert TestIO.binread() === <<>> 276 | end 277 | 278 | test "get_status that raises exception" do 279 | exception = ArgumentError.exception([message: "hello"]) 280 | pid = Core.spawn_link(__MODULE__, fn() -> raise(exception) end) 281 | assert_raise Core.Sys.CallbackError, 282 | ~r"^failure in &Core\.SysTest\.system_get_data/1\n \*\* \(ArgumentError\) hello\n"m, 283 | fn() -> Core.Sys.get_status(pid) end 284 | assert close(pid) === :ok 285 | assert TestIO.binread() === <<>> 286 | end 287 | 288 | test "get_status that throws" do 289 | pid = Core.spawn_link(__MODULE__, fn() -> throw(:hello) end) 290 | assert_raise Core.Sys.CallbackError, 291 | ~r"^failure in &Core\.SysTest\.system_get_data/1\n \*\* \(throw\) :hello\n"m, 292 | fn() -> Core.Sys.get_status(pid) end 293 | assert close(pid) === :ok 294 | assert TestIO.binread() === <<>> 295 | end 296 | 297 | test "get_status that exits" do 298 | pid = Core.spawn_link(__MODULE__, fn() -> exit(:hello) end) 299 | assert_raise Core.Sys.CallbackError, 300 | ~r"^failure in &Core\.SysTest\.system_get_data/1\n \*\* \(exit\) :hello\n"m, 301 | fn() -> Core.Sys.get_status(pid) end 302 | assert close(pid) === :ok 303 | assert TestIO.binread() === <<>> 304 | end 305 | 306 | test ":sys get_status" do 307 | ref = make_ref() 308 | { :ok, pid } = Core.start_link(__MODULE__, fn() -> ref end) 309 | parent = self() 310 | { :dictionary, pdict } = Process.info(pid, :dictionary) 311 | assert { :status, ^pid, { :module, Core.Sys }, 312 | [^pdict, :running, ^parent, [], status] } = :sys.get_status(pid) 313 | # copy the format of gen_* :sys.status's. Length-3 list, first term is 314 | # header tuple, second is general information in :data tuple supplied by all 315 | # callbacks, and third is specific to the callback. 316 | assert [{ :header, header }, { :data, data1 }, { :data, data2 }] = status 317 | assert header === String.to_char_list("Status for " <> 318 | "#{inspect(__MODULE__)} #{inspect(pid)}") 319 | assert List.keyfind(data1, 'Status', 0) === { 'Status', :running } 320 | assert List.keyfind(data1, 'Parent', 0) === { 'Parent', self() } 321 | assert List.keyfind(data1, 'Logged events', 0) === { 'Logged events', [] } 322 | assert List.keyfind(data1, 'Statistics', 0) === { 'Statistics', 323 | :no_statistics } 324 | assert List.keyfind(data1, 'Name', 0) === { 'Name', pid } 325 | assert List.keyfind(data1, 'Module', 0) === { 'Module', __MODULE__ } 326 | assert List.keyfind(data2, 'Module data', 0) === { 'Module data', ref } 327 | assert List.keyfind(data2, 'Module error', 0) === nil 328 | assert close(pid) === :ok 329 | assert TestIO.binread() === <<>> 330 | end 331 | 332 | test ":sys.get_status with exception" do 333 | exception = ArgumentError.exception([message: "hello"]) 334 | pid = Core.spawn_link(__MODULE__, fn() -> raise(exception) end) 335 | assert { :status, ^pid, { :module, Core.Sys }, 336 | [_, _, _, _, status] } = :sys.get_status(pid) 337 | assert [{ :header, _header }, { :data, _data1 }, { :data, data2 }] = status 338 | # error like 17.0 format for :sys.get_state/replace_stats 339 | action = &__MODULE__.system_get_data/1 340 | error = List.keyfind(data2, 'Module error', 0) 341 | assert match?({'Module error', 342 | {:callback_failed, {Core.Sys, :format_status}, 343 | {:error, %Core.Sys.CallbackError{action: ^action, 344 | kind: :error, payload: ^exception, stacktrace: [_|_]}}}}, error) 345 | assert close(pid) === :ok 346 | assert TestIO.binread() === <<>> 347 | end 348 | 349 | test ":sys.get_status with throw" do 350 | pid = Core.spawn_link(__MODULE__, fn() -> throw(:hello) end) 351 | assert { :status, ^pid, { :module, Core.Sys }, 352 | [_, _, _, _, status] } = :sys.get_status(pid) 353 | assert [{ :header, _header }, { :data, _data1 }, { :data, data2 }] = status 354 | # error like 17.0 format for :sys.get_state/replace_stats 355 | action = &__MODULE__.system_get_data/1 356 | error = List.keyfind(data2, 'Module error', 0) 357 | assert match?({'Module error', 358 | {:callback_failed, {Core.Sys, :format_status}, 359 | {:error, %Core.Sys.CallbackError{action: ^action, 360 | kind: :throw, payload: :hello, stacktrace: [_|_]}}}}, error) 361 | assert close(pid) === :ok 362 | assert TestIO.binread() === <<>> 363 | end 364 | 365 | test ":sys.get_status with exit" do 366 | pid = Core.spawn_link(__MODULE__, fn() -> exit(:hello) end) 367 | assert { :status, ^pid, { :module, Core.Sys }, 368 | [_, _, _, _, status] } = :sys.get_status(pid) 369 | assert [{ :header, _header }, { :data, _data1 }, { :data, data2 }] = status 370 | # error like 17.0 format for :sys.get_state/replace_stats 371 | action = &__MODULE__.system_get_data/1 372 | error = List.keyfind(data2, 'Module error', 0) 373 | assert match?({'Module error', 374 | {:callback_failed, {Core.Sys, :format_status}, 375 | {:error, %Core.Sys.CallbackError{action: ^action, 376 | kind: :exit, payload: :hello, stacktrace: [_|_]}}}}, error) 377 | assert close(pid) === :ok 378 | assert TestIO.binread() === <<>> 379 | end 380 | 381 | test ":sys.get_status with log" do 382 | ref = make_ref() 383 | pid = Core.spawn_link(__MODULE__, fn() -> ref end, 384 | [{ :debug, [{ :log, 10 }] }]) 385 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 386 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 387 | assert { :status, ^pid, { :module, Core.Sys }, 388 | [_, _, _, debug, status] } = :sys.get_status(pid) 389 | assert [{ :header, _header }, { :data, data1 }, { :data, _data2 }] = status 390 | # This is how gen_* displays the log 391 | sys_log = :sys.get_debug(:log, debug, []) 392 | assert List.keyfind(data1, 'Logged events', 0) === { 'Logged events', 393 | sys_log } 394 | assert close(pid) === :ok 395 | assert TestIO.binread() === <<>> 396 | end 397 | 398 | test "get_status :gen_server" do 399 | ref = make_ref() 400 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 401 | status = Core.Sys.get_status(pid) 402 | assert status[:module] === :gen_server 403 | assert status[:data] === ref 404 | assert status[:pid] === pid 405 | assert status[:name] === pid 406 | assert status[:log] === [] 407 | assert status[:stats] === nil 408 | assert status[:sys_status] === :running 409 | assert status[:parent] === self() 410 | assert Map.has_key?(status, :dictionary) 411 | assert close(pid) === :ok 412 | assert TestIO.binread() === <<>> 413 | end 414 | 415 | test "get_status :gen_event" do 416 | ref = make_ref() 417 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 418 | status = Core.Sys.get_status(pid) 419 | assert status[:module] === :gen_event 420 | assert status[:data] === [{ GE, false, ref}] 421 | assert status[:pid] === pid 422 | assert status[:name] === pid 423 | assert status[:log] === [] 424 | assert status[:stats] === nil 425 | assert status[:sys_status] === :running 426 | assert status[:parent] === self() 427 | assert Map.has_key?(status, :dictionary) 428 | assert close(pid) === :ok 429 | assert TestIO.binread() === <<>> 430 | end 431 | 432 | test "get_status :gen_fsm" do 433 | ref = make_ref() 434 | { :ok, pid} = GFSM.start_link(fn() -> {:ok, :state, ref} end) 435 | status = Core.Sys.get_status(pid) 436 | assert status[:module] === :gen_fsm 437 | assert status[:data] === { :state, ref } 438 | assert status[:pid] === pid 439 | assert status[:name] === pid 440 | assert status[:log] === [] 441 | assert status[:stats] === nil 442 | assert status[:sys_status] === :running 443 | assert status[:parent] === self() 444 | assert Map.has_key?(status, :dictionary) 445 | assert close(pid) === :ok 446 | assert TestIO.binread() === <<>> 447 | end 448 | 449 | test "get_status :gen_server with log" do 450 | ref = make_ref() 451 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end, [{ :log, 10 }]) 452 | send(pid, { :msg, 1 }) 453 | send(pid, { :msg, 2 }) 454 | status = Core.Sys.get_status(pid) 455 | assert status[:log] === [{ :in, { :msg, 1 } }, { :noreply, ref }, 456 | { :in, { :msg, 2 } }, { :noreply, ref }] 457 | assert close(pid) === :ok 458 | assert TestIO.binread() === <<>> 459 | end 460 | 461 | test "get_status :gen_fsm with log" do 462 | ref = make_ref() 463 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end, 464 | [{ :log, 10 }]) 465 | send(pid, { :msg, 1 }) 466 | send(pid, { :msg, 2 }) 467 | status = Core.Sys.get_status(pid) 468 | assert status[:log] === [{ :in, { :msg, 1 } }, :return, 469 | { :in, { :msg, 2 } }, :return] 470 | assert close(pid) === :ok 471 | assert TestIO.binread() === <<>> 472 | end 473 | 474 | test "get_data" do 475 | ref = make_ref() 476 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 477 | assert Core.Sys.get_data(pid) === ref 478 | assert close(pid) === :ok 479 | assert TestIO.binread() === <<>> 480 | end 481 | 482 | test "change_data" do 483 | ref1 = make_ref() 484 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 485 | ref2 = make_ref() 486 | fun = fn() -> fn() -> ref2 end end 487 | assert Core.Sys.suspend(pid) === :ok 488 | assert Core.Sys.change_data(pid, __MODULE__, nil, fun) === :ok 489 | assert Core.Sys.resume(pid) === :ok 490 | assert Core.call(pid, __MODULE__, :eval, 500) === ref2, "data not changed" 491 | assert close(pid) === :ok 492 | assert TestIO.binread() === <<>> 493 | end 494 | 495 | test "change_data that raises exception" do 496 | ref = make_ref() 497 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 498 | Core.Sys.suspend(pid) 499 | exception = ArgumentError.exception([message: "hello"]) 500 | extra = fn() -> raise(exception) end 501 | assert_raise Core.Sys.CallbackError, 502 | ~r"^failure in &Core\.SysTest\.system_change_data/4\n \*\* \(ArgumentError\) hello\n"m, 503 | fn() -> Core.Sys.change_data(pid, __MODULE__, nil, extra) end 504 | Core.Sys.resume(pid) 505 | assert Core.call(pid, __MODULE__, :eval, 500) === ref, "data changed" 506 | assert close(pid) === :ok 507 | assert TestIO.binread() === <<>> 508 | end 509 | 510 | test "change_data that throws" do 511 | ref = make_ref() 512 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 513 | Core.Sys.suspend(pid) 514 | extra = fn() -> throw(:hello) end 515 | assert_raise Core.Sys.CallbackError, 516 | ~r"^failure in &Core\.SysTest\.system_change_data/4\n \*\* \(throw\) :hello\n"m, 517 | fn() -> Core.Sys.change_data(pid, __MODULE__, nil, extra) end 518 | Core.Sys.resume(pid) 519 | assert Core.call(pid, __MODULE__, :eval, 500) === ref, "data changed" 520 | assert close(pid) === :ok 521 | assert TestIO.binread() === <<>> 522 | end 523 | 524 | test "change_data that exits" do 525 | ref = make_ref() 526 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 527 | Core.Sys.suspend(pid) 528 | extra = fn() -> exit(:hello) end 529 | assert_raise Core.Sys.CallbackError, 530 | ~r"^failure in &Core\.SysTest\.system_change_data/4\n \*\* \(exit\) :hello\n"m, 531 | fn() -> Core.Sys.change_data(pid, __MODULE__, nil, extra) end 532 | Core.Sys.resume(pid) 533 | assert Core.call(pid, __MODULE__, :eval, 500) === ref, "data changed" 534 | assert close(pid) === :ok 535 | assert TestIO.binread() === <<>> 536 | end 537 | 538 | test "change_data while running" do 539 | ref1 = make_ref() 540 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 541 | ref2 = make_ref() 542 | extra = fn() -> fn() -> ref2 end end 543 | assert_raise ArgumentError, "#{inspect(pid)} is running", 544 | fn() -> Core.Sys.change_data(pid, __MODULE__, nil, extra) end 545 | assert Core.call(pid, __MODULE__, :eval, 500) === ref1, "data changed" 546 | assert close(pid) === :ok 547 | assert TestIO.binread() === <<>> 548 | end 549 | 550 | test "change_data :gen_server" do 551 | ref1 = make_ref() 552 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref1 } end) 553 | Core.Sys.suspend(pid) 554 | ref2 = make_ref() 555 | extra = fn() -> { :ok, ref2 } end 556 | assert Core.Sys.change_data(pid, GS, nil, extra) === :ok 557 | Core.Sys.resume(pid) 558 | assert :sys.get_state(pid) === ref2 559 | assert close(pid) === :ok 560 | assert TestIO.binread() === <<>> 561 | end 562 | 563 | test "change_data :gen_server with raise" do 564 | ref1 = make_ref() 565 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref1 } end) 566 | Core.Sys.suspend(pid) 567 | exception = ArgumentError.exception([message: "hello"]) 568 | extra = fn() -> raise(exception) end 569 | assert_raise Core.Sys.CallbackError, 570 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(ArgumentError\) hello\n"m, 571 | fn() -> Core.Sys.change_data(pid, GS, nil, extra) end 572 | Core.Sys.resume(pid) 573 | assert close(pid) === :ok 574 | assert TestIO.binread() === <<>> 575 | end 576 | 577 | test "change_data :gen_server with erlang badarg" do 578 | ref1 = make_ref() 579 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref1 } end) 580 | Core.Sys.suspend(pid) 581 | extra = fn() -> :erlang.error(:badarg, []) end 582 | assert_raise Core.Sys.CallbackError, 583 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(ArgumentError\) argument error\n"m, 584 | fn() -> Core.Sys.change_data(pid, GS, nil, extra) end 585 | Core.Sys.resume(pid) 586 | assert close(pid) === :ok 587 | assert TestIO.binread() === <<>> 588 | end 589 | 590 | test "change_data :gen_server with erlang error" do 591 | ref = make_ref() 592 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 593 | Core.Sys.suspend(pid) 594 | extra = fn() -> :erlang.error(:custom_erlang, []) end 595 | assert_raise Core.Sys.CallbackError, 596 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(ErlangError\) erlang error: :custom_erlang\n"m, 597 | fn() -> Core.Sys.change_data(pid, GS, nil, extra) end 598 | Core.Sys.resume(pid) 599 | assert close(pid) === :ok 600 | assert TestIO.binread() === <<>> 601 | end 602 | 603 | test "change_data :gen_server with exit" do 604 | ref = make_ref() 605 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 606 | Core.Sys.suspend(pid) 607 | extra = fn() -> exit(:exit_reason) end 608 | assert_raise Core.Sys.CallbackError, 609 | "failure in unknown function\n ** (exit) :exit_reason", 610 | fn() -> Core.Sys.change_data(pid, GS, nil, extra) end 611 | Core.Sys.resume(pid) 612 | assert close(pid) === :ok 613 | assert TestIO.binread() === <<>> 614 | end 615 | 616 | test "change_data :gen_server with bad return" do 617 | ref = make_ref() 618 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 619 | Core.Sys.suspend(pid) 620 | extra = fn() -> :badreturn end 621 | assert_raise Core.Sys.CallbackError, 622 | "failure in unknown function\n ** (ErlangError) erlang error: :badreturn", 623 | fn() -> Core.Sys.change_data(pid, GS, nil, extra) end 624 | Core.Sys.resume(pid) 625 | assert close(pid) === :ok 626 | assert TestIO.binread() === <<>> 627 | end 628 | 629 | test "change_data :gen_event" do 630 | ref1 = make_ref() 631 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref1 } end) 632 | :ok = Core.Sys.suspend(pid) 633 | ref2 = make_ref() 634 | extra = fn() -> { :ok, ref2 } end 635 | assert Core.Sys.change_data(pid, GE, nil, extra) === :ok 636 | Core.Sys.resume(pid) 637 | assert :sys.get_state(pid) === [{GE, false, ref2 }] 638 | assert close(pid) === :ok 639 | assert TestIO.binread() === <<>> 640 | end 641 | 642 | test "change_data :gen_event with raise" do 643 | ref = make_ref() 644 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 645 | Core.Sys.suspend(pid) 646 | exception = ArgumentError.exception([message: "hello"]) 647 | extra = fn() -> raise(exception) end 648 | assert_raise Core.Sys.CallbackError, 649 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(ArgumentError\) hello\n"m, 650 | fn() -> Core.Sys.change_data(pid, GE, nil, extra) end 651 | Core.Sys.resume(pid) 652 | assert close(pid) === :ok 653 | assert TestIO.binread() === <<>> 654 | end 655 | 656 | test "change_data :gen_event with erlang badarg" do 657 | ref = make_ref() 658 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 659 | Core.Sys.suspend(pid) 660 | extra = fn() -> :erlang.error(:badarg, []) end 661 | assert_raise Core.Sys.CallbackError, 662 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(ArgumentError\) argument error\n"m, 663 | fn() -> Core.Sys.change_data(pid, GE, nil, extra) end 664 | Core.Sys.resume(pid) 665 | assert close(pid) === :ok 666 | assert TestIO.binread() === <<>> 667 | end 668 | 669 | test "change_data :gen_event with erlang error" do 670 | ref = make_ref() 671 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 672 | Core.Sys.suspend(pid) 673 | extra = fn() -> :erlang.error(:custom_erlang, []) end 674 | assert_raise Core.Sys.CallbackError, 675 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(ErlangError\) erlang error: :custom_erlang\n"m, 676 | fn() -> Core.Sys.change_data(pid, GE, nil, extra) end 677 | Core.Sys.resume(pid) 678 | assert close(pid) === :ok 679 | assert TestIO.binread() === <<>> 680 | end 681 | 682 | test "change_data :gen_event with exit" do 683 | ref = make_ref() 684 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 685 | Core.Sys.suspend(pid) 686 | extra = fn() -> exit(:exit_reason) end 687 | assert_raise Core.Sys.CallbackError, 688 | ~r"^failure in unknown function\n \*\* \(exit\) :exit_reason$"m, 689 | fn() -> Core.Sys.change_data(pid, GE, nil, extra) end 690 | Core.Sys.resume(pid) 691 | assert close(pid) === :ok 692 | assert TestIO.binread() === <<>> 693 | end 694 | 695 | test "change_data :gen_event with bad return" do 696 | ref = make_ref() 697 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 698 | Core.Sys.suspend(pid) 699 | extra = fn() -> :badreturn end 700 | assert_raise Core.Sys.CallbackError, 701 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(MatchError\) no match of right hand side value: :badreturn\n"m, 702 | fn() -> Core.Sys.change_data(pid, GE, nil, extra) end 703 | Core.Sys.resume(pid) 704 | assert close(pid) === :ok 705 | assert TestIO.binread() === <<>> 706 | end 707 | 708 | test "change_data :gen_fsm" do 709 | ref1 = make_ref() 710 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref1 } end) 711 | Core.Sys.suspend(pid) 712 | ref2 = make_ref() 713 | extra = fn() -> { :ok, :state, ref2 } end 714 | assert Core.Sys.change_data(pid, GFSM, nil, extra) === :ok 715 | Core.Sys.resume(pid) 716 | assert :sys.get_state(pid) === { :state, ref2 } 717 | assert close(pid) === :ok 718 | assert TestIO.binread() === <<>> 719 | end 720 | 721 | test "change_data :gen_fsm with raise" do 722 | ref = make_ref() 723 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 724 | Core.Sys.suspend(pid) 725 | exception = ArgumentError.exception([message: "hello"]) 726 | extra = fn() -> raise(exception) end 727 | assert_raise Core.Sys.CallbackError, 728 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(ArgumentError\) hello\n"m, 729 | fn() -> Core.Sys.change_data(pid, GFSM, nil, extra) end 730 | Core.Sys.resume(pid) 731 | assert close(pid) === :ok 732 | assert TestIO.binread() === <<>> 733 | end 734 | 735 | test "change_data :gen_fsm with erlang badarg" do 736 | ref = make_ref() 737 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 738 | Core.Sys.suspend(pid) 739 | extra = fn() -> :erlang.error(:badarg, []) end 740 | assert_raise Core.Sys.CallbackError, 741 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(ArgumentError\) argument error"m, 742 | fn() -> Core.Sys.change_data(pid, GFSM, nil, extra) end 743 | Core.Sys.resume(pid) 744 | assert close(pid) === :ok 745 | assert TestIO.binread() === <<>> 746 | end 747 | 748 | test "change_data :gen_fsm with erlang error" do 749 | ref = make_ref() 750 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 751 | Core.Sys.suspend(pid) 752 | extra = fn() -> :erlang.error(:custom_erlang, []) end 753 | assert_raise Core.Sys.CallbackError, 754 | ~r"^failure in unknown function\n \*\* \(exit\) an exception was raised:\n \*\* \(ErlangError\) erlang error: :custom_erlang"m, 755 | fn() -> Core.Sys.change_data(pid, GFSM, nil, extra) end 756 | Core.Sys.resume(pid) 757 | assert close(pid) === :ok 758 | assert TestIO.binread() === <<>> 759 | end 760 | 761 | test "change_data :gen_fsm with exit" do 762 | ref = make_ref() 763 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 764 | Core.Sys.suspend(pid) 765 | extra = fn() -> exit(:exit_reason) end 766 | assert_raise Core.Sys.CallbackError, 767 | "failure in unknown function\n ** (exit) :exit_reason", 768 | fn() -> Core.Sys.change_data(pid, GFSM, nil, extra) end 769 | Core.Sys.resume(pid) 770 | assert close(pid) === :ok 771 | assert TestIO.binread() === <<>> 772 | end 773 | 774 | test "change_data :gen_fsm with bad return" do 775 | ref = make_ref() 776 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 777 | Core.Sys.suspend(pid) 778 | extra = fn() -> :badreturn end 779 | assert_raise Core.Sys.CallbackError, 780 | "failure in unknown function\n ** (ErlangError) erlang error: :badreturn", 781 | fn() -> Core.Sys.change_data(pid, GFSM, nil, extra) end 782 | Core.Sys.resume(pid) 783 | assert close(pid) === :ok 784 | assert TestIO.binread() === <<>> 785 | end 786 | 787 | test "parent exit" do 788 | ref = make_ref() 789 | fun = fn() -> Process.flag(:trap_exit, true) ; ref end 790 | pid = Core.spawn_link(__MODULE__, fun) 791 | assert Core.call(pid, __MODULE__, :eval, 500) === ref, "trap_exit not set" 792 | trap = Process.flag(:trap_exit, true) 793 | Process.exit(pid, :exit_reason) 794 | assert_receive { :EXIT, ^pid, reason }, 100, "process did not exit" 795 | assert reason === :exit_reason 796 | Process.flag(:trap_exit, trap) 797 | assert TestIO.binread() === <<>> 798 | end 799 | 800 | test "get_log with 0 events" do 801 | ref1 = make_ref() 802 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 803 | [{ :debug, [{ :log, 10 }] }]) 804 | assert Core.Sys.get_log(pid) === [] 805 | assert close(pid) === :ok 806 | assert TestIO.binread() === <<>> 807 | end 808 | 809 | test "get_log with 2 events" do 810 | ref1 = make_ref() 811 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 812 | [{ :debug, [{ :log, 10 }] }]) 813 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 814 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 815 | assert Core.Sys.get_log(pid) === [{ :event, 1 }, { :event, 2 }] 816 | assert close(pid) === :ok 817 | assert TestIO.binread() === <<>> 818 | end 819 | 820 | test "get_log with no logging and 2 events" do 821 | ref1 = make_ref() 822 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 823 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 824 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 825 | assert Core.Sys.get_log(pid) === [] 826 | assert close(pid) === :ok 827 | assert TestIO.binread() === <<>> 828 | end 829 | 830 | test "get_log :gen_server with 0 events" do 831 | ref = make_ref() 832 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end, [{ :log, 10 }]) 833 | assert Core.Sys.get_log(pid) === [] 834 | assert close(pid) === :ok 835 | assert TestIO.binread() === <<>> 836 | end 837 | 838 | test "get_log :gen_server with 2 events" do 839 | ref = make_ref() 840 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end, [{ :log, 10 }]) 841 | send(pid, 1) 842 | send(pid, 2) 843 | assert Core.Sys.get_log(pid) === [{ :in, 1 }, { :noreply, ref }, 844 | { :in, 2 }, { :noreply, ref }] 845 | assert close(pid) === :ok 846 | assert TestIO.binread() === <<>> 847 | end 848 | 849 | test "get_log :gen_server with no logging" do 850 | ref = make_ref() 851 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 852 | assert Core.Sys.get_log(pid) === [] 853 | assert close(pid) === :ok 854 | assert TestIO.binread() === <<>> 855 | end 856 | 857 | test "get_log :gen_event with no logging (can never log)" do 858 | ref = make_ref() 859 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 860 | assert Core.Sys.get_log(pid) === [] 861 | assert close(pid) === :ok 862 | assert TestIO.binread() === <<>> 863 | end 864 | 865 | test "get_log :gen_fsm with 0 events" do 866 | ref = make_ref() 867 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end, 868 | [{ :log, 10 }]) 869 | assert Core.Sys.get_log(pid) === [] 870 | assert close(pid) === :ok 871 | assert TestIO.binread() === <<>> 872 | end 873 | 874 | test "get_log :gen_fsm with 2 events" do 875 | ref = make_ref() 876 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end, 877 | [{ :log, 10 }]) 878 | send(pid, 1) 879 | send(pid, 2) 880 | assert Core.Sys.get_log(pid) === [{ :in, 1 }, :return, { :in, 2 }, :return] 881 | assert close(pid) === :ok 882 | assert TestIO.binread() === <<>> 883 | end 884 | 885 | test "get_log :gen_fsm with no logging" do 886 | ref = make_ref() 887 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 888 | assert Core.Sys.get_log(pid) === [] 889 | assert close(pid) === :ok 890 | assert TestIO.binread() === <<>> 891 | end 892 | 893 | test "print_log with 2 events" do 894 | ref1 = make_ref() 895 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 896 | [{ :debug, [{ :log, 10 }] }]) 897 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 898 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 899 | assert Core.Sys.print_log(pid) === :ok 900 | assert close(pid) === :ok 901 | report = "** Core.Debug #{inspect(pid)} event log:\n" <> 902 | "** Core.Debug #{inspect(pid)} #{inspect({ :event, 1 })}\n" <> 903 | "** Core.Debug #{inspect(pid)} #{inspect({ :event, 2 })}\n" <> 904 | "\n" 905 | assert TestIO.binread() === report 906 | end 907 | 908 | test "print_log with 0 events" do 909 | ref1 = make_ref() 910 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 911 | [{ :debug, [{ :log, 10 }] }]) 912 | assert Core.Sys.print_log(pid) === :ok 913 | assert close(pid) === :ok 914 | report = "** Core.Debug #{inspect(pid)} event log is empty\n" <> 915 | "\n" 916 | assert TestIO.binread() === report 917 | end 918 | 919 | test "print_log with no logging and 2 events" do 920 | ref1 = make_ref() 921 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 922 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 923 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 924 | assert Core.Sys.print_log(pid) === :ok 925 | assert close(pid) === :ok 926 | report = "** Core.Debug #{inspect(pid)} event log is empty\n" <> 927 | "\n" 928 | assert TestIO.binread() === report 929 | end 930 | 931 | test "print_log with cast message in" do 932 | ref1 = make_ref() 933 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 934 | [{ :debug, [{ :log, 10 }] }]) 935 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello } }, 500) 936 | assert Core.Sys.print_log(pid) === :ok 937 | assert close(pid) === :ok 938 | report = "** Core.Debug #{inspect(pid)} event log:\n" <> 939 | "** Core.Debug #{inspect(pid)} message in: :hello\n" <> 940 | "\n" 941 | assert TestIO.binread() === report 942 | end 943 | 944 | test "print_log with call message in" do 945 | ref1 = make_ref() 946 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 947 | [{ :debug, [{ :log, 10 }] }]) 948 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello, self() } }, 500) 949 | assert Core.Sys.print_log(pid) === :ok 950 | assert close(pid) === :ok 951 | report = "** Core.Debug #{inspect(pid)} event log:\n" <> 952 | "** Core.Debug #{inspect(pid)} message in (from #{inspect(self())}): " <> 953 | ":hello\n" <> 954 | "\n" 955 | assert TestIO.binread() === report 956 | end 957 | 958 | test "print_log with message out" do 959 | ref1 = make_ref() 960 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 961 | [{ :debug, [{ :log, 10 }] }]) 962 | :ok = Core.call(pid, __MODULE__, { :event, { :out, :hello, self() } }, 500) 963 | assert Core.Sys.print_log(pid) === :ok 964 | assert close(pid) === :ok 965 | report = "** Core.Debug #{inspect(pid)} event log:\n" <> 966 | "** Core.Debug #{inspect(pid)} message out (to #{inspect(self())}): " <> 967 | ":hello\n" <> 968 | "\n" 969 | assert TestIO.binread() === report 970 | end 971 | 972 | test "print_log :gen_server with 0 events" do 973 | ref = make_ref() 974 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end, [{ :log, 10 }]) 975 | assert Core.Sys.print_log(pid) === :ok 976 | assert close(pid) === :ok 977 | report = "** Core.Debug #{inspect(pid)} event log is empty\n" <> 978 | "\n" 979 | assert TestIO.binread() === report 980 | end 981 | 982 | test "print_log :gen_server with 2 events" do 983 | ref = make_ref() 984 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end, [{ :log, 10 }]) 985 | send(pid, 1) 986 | send(pid, 2) 987 | assert Core.Sys.print_log(pid) === :ok 988 | assert close(pid) === :ok 989 | erl_pid = inspect_erl(pid) 990 | erl_ref = inspect_erl(ref) 991 | report = "** Core.Debug #{inspect(pid)} event log:\n" <> 992 | "*DBG* #{erl_pid} got 1\n" <> 993 | "*DBG* #{erl_pid} new state #{erl_ref}\n" <> 994 | "*DBG* #{erl_pid} got 2\n" <> 995 | "*DBG* #{erl_pid} new state #{erl_ref}\n" <> 996 | "\n" 997 | assert TestIO.binread() === report 998 | end 999 | 1000 | test "print_log :gen_server with no logging" do 1001 | ref = make_ref() 1002 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 1003 | assert Core.Sys.print_log(pid) === :ok 1004 | assert close(pid) === :ok 1005 | report = "** Core.Debug #{inspect(pid)} event log is empty\n" <> 1006 | "\n" 1007 | assert TestIO.binread() === report 1008 | end 1009 | 1010 | test "print_log :gen_event with no logging (can never log)" do 1011 | ref = make_ref() 1012 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 1013 | assert Core.Sys.print_log(pid) === :ok 1014 | assert close(pid) === :ok 1015 | report = "** Core.Debug #{inspect(pid)} event log is empty\n" <> 1016 | "\n" 1017 | assert TestIO.binread() === report 1018 | end 1019 | 1020 | test "print_log :gen_fsm with 0 events" do 1021 | ref = make_ref() 1022 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end, 1023 | [{ :log, 10 }]) 1024 | assert Core.Sys.print_log(pid) === :ok 1025 | assert close(pid) === :ok 1026 | report = "** Core.Debug #{inspect(pid)} event log is empty\n" <> 1027 | "\n" 1028 | assert TestIO.binread() === report 1029 | end 1030 | 1031 | test "print_log :gen_fsm with 2 events" do 1032 | ref = make_ref() 1033 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end, 1034 | [{ :log, 10 }]) 1035 | send(pid, 1) 1036 | send(pid, 2) 1037 | assert Core.Sys.print_log(pid) === :ok 1038 | assert close(pid) === :ok 1039 | erl_pid = inspect_erl(pid) 1040 | report = "** Core.Debug #{inspect(pid)} event log:\n" <> 1041 | "*DBG* #{erl_pid} got 1 in state state\n" <> 1042 | "*DBG* #{erl_pid} switched to state state\n" <> 1043 | "*DBG* #{erl_pid} got 2 in state state\n" <> 1044 | "*DBG* #{erl_pid} switched to state state\n" <> 1045 | "\n" 1046 | assert TestIO.binread() === report 1047 | end 1048 | 1049 | test "print_log :gen_fsm with no logging" do 1050 | ref = make_ref() 1051 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 1052 | assert Core.Sys.print_log(pid) === :ok 1053 | assert close(pid) === :ok 1054 | report = "** Core.Debug #{inspect(pid)} event log is empty\n" <> 1055 | "\n" 1056 | assert TestIO.binread() === report 1057 | end 1058 | 1059 | test "set_log 10 with 2 events" do 1060 | ref1 = make_ref() 1061 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 1062 | assert Core.Sys.set_log(pid, 10) === :ok 1063 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 1064 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 1065 | assert Core.Sys.get_log(pid) === [{ :event, 1 }, { :event, 2 }] 1066 | assert close(pid) === :ok 1067 | assert TestIO.binread() === <<>> 1068 | end 1069 | 1070 | test "set_log 1 with 2 events" do 1071 | ref1 = make_ref() 1072 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 1073 | assert Core.Sys.set_log(pid, 1) === :ok 1074 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 1075 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 1076 | assert Core.Sys.get_log(pid) === [{ :event, 2 }] 1077 | assert close(pid) === :ok 1078 | assert TestIO.binread() === <<>> 1079 | end 1080 | 1081 | test "set_log 0 with 1 event before and 1 event after" do 1082 | ref1 = make_ref() 1083 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1084 | [{ :debug, [{ :log, 10 }] }]) 1085 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 1086 | assert Core.Sys.set_log(pid, 0) === :ok 1087 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 1088 | assert Core.Sys.get_log(pid) === [] 1089 | assert close(pid) === :ok 1090 | assert TestIO.binread() === <<>> 1091 | end 1092 | 1093 | test "set_log 10 :gen_server with 2 events" do 1094 | ref = make_ref() 1095 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 1096 | assert Core.Sys.set_log(pid, 10) === :ok 1097 | send(pid, 1) 1098 | send(pid, 2) 1099 | assert Core.Sys.get_log(pid) === [{ :in, 1 }, { :noreply, ref }, 1100 | { :in, 2 }, { :noreply, ref }] 1101 | assert close(pid) === :ok 1102 | assert TestIO.binread() === <<>> 1103 | end 1104 | 1105 | test "set_log 1 :gen_server with 2 events" do 1106 | ref = make_ref() 1107 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 1108 | assert Core.Sys.set_log(pid, 1) === :ok 1109 | send(pid, 1) 1110 | assert Core.Sys.get_log(pid) === [{ :noreply, ref }] 1111 | assert close(pid) === :ok 1112 | assert TestIO.binread() === <<>> 1113 | end 1114 | 1115 | test "set_log 0 :gen_server with 2 events before and 2 event after" do 1116 | ref = make_ref() 1117 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end, [{ :log, 10 }]) 1118 | send(pid, 1) 1119 | assert Core.Sys.set_log(pid, 0) === :ok 1120 | send(pid, 2) 1121 | assert Core.Sys.get_log(pid) === [] 1122 | assert close(pid) === :ok 1123 | assert TestIO.binread() === <<>> 1124 | end 1125 | 1126 | test "set_log 10 :gen_fsm with 4 events" do 1127 | ref = make_ref() 1128 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 1129 | assert Core.Sys.set_log(pid, 10) === :ok 1130 | send(pid, 1) 1131 | send(pid, 2) 1132 | assert Core.Sys.get_log(pid) === [{ :in, 1 }, :return, { :in, 2 }, :return] 1133 | assert close(pid) === :ok 1134 | assert TestIO.binread() === <<>> 1135 | end 1136 | 1137 | test "set_log 1 :gen_fsm with 2 events" do 1138 | ref = make_ref() 1139 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 1140 | assert Core.Sys.set_log(pid, 1) === :ok 1141 | send(pid, 1) 1142 | assert Core.Sys.get_log(pid) === [:return] 1143 | assert close(pid) === :ok 1144 | assert TestIO.binread() === <<>> 1145 | end 1146 | 1147 | test "set_log 0 :gen_fsm with 2 events before and 2 event after" do 1148 | ref = make_ref() 1149 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end, 1150 | [{ :log, 10 }]) 1151 | send(pid, 1) 1152 | assert Core.Sys.set_log(pid, 0) === :ok 1153 | send(pid, 2) 1154 | assert Core.Sys.get_log(pid) === [] 1155 | assert close(pid) === :ok 1156 | assert TestIO.binread() === <<>> 1157 | end 1158 | 1159 | test "get_stats with 0 events" do 1160 | ref1 = make_ref() 1161 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1162 | [{ :debug, [{ :stats, true }] }]) 1163 | stats = Core.Sys.get_stats(pid) 1164 | assert stats[:in] === 0 1165 | assert stats[:out] === 0 1166 | assert is_integer(stats[:reductions]) 1167 | assert stats[:start_time] <= stats[:current_time] 1168 | assert Map.size(stats) === 5 1169 | assert close(pid) === :ok 1170 | assert TestIO.binread() === <<>> 1171 | end 1172 | 1173 | test "get_stats with no stats" do 1174 | ref = make_ref() 1175 | pid = Core.spawn_link(__MODULE__, fn() -> ref end) 1176 | assert Core.Sys.get_stats(pid) === nil 1177 | assert close(pid) === :ok 1178 | assert TestIO.binread() === <<>> 1179 | end 1180 | 1181 | test "get_stats with cast message in" do 1182 | ref1 = make_ref() 1183 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1184 | [{ :debug, [{ :stats, true }] }]) 1185 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello } }, 500) 1186 | stats = Core.Sys.get_stats(pid) 1187 | assert stats[:in] === 1 1188 | assert close(pid) === :ok 1189 | assert TestIO.binread() === <<>> 1190 | end 1191 | 1192 | test "get_stats with call message in" do 1193 | ref1 = make_ref() 1194 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1195 | [{ :debug, [{ :stats, true }] }]) 1196 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello, self() } }, 500) 1197 | stats = Core.Sys.get_stats(pid) 1198 | assert stats[:in] === 1 1199 | assert close(pid) === :ok 1200 | assert TestIO.binread() === <<>> 1201 | end 1202 | 1203 | test "get_stats with message out" do 1204 | ref1 = make_ref() 1205 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1206 | [{ :debug, [{ :stats, true }] }]) 1207 | :ok = Core.call(pid, __MODULE__, { :event, { :out, :hello, self() } }, 500) 1208 | stats = Core.Sys.get_stats(pid) 1209 | assert stats[:out] === 1 1210 | assert close(pid) === :ok 1211 | assert TestIO.binread() === <<>> 1212 | end 1213 | 1214 | test "get_stats :gen_server with no stats" do 1215 | ref = make_ref() 1216 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end) 1217 | assert Core.Sys.get_stats(pid) === nil 1218 | assert close(pid) === :ok 1219 | assert TestIO.binread() === <<>> 1220 | end 1221 | 1222 | test "get_stats :gen_server with 1 message in" do 1223 | ref = make_ref() 1224 | { :ok, pid } = GS.start_link(fn() -> { :ok, ref } end, [:statistics]) 1225 | send(pid, 1) 1226 | stats = Core.Sys.get_stats(pid) 1227 | assert stats[:in] === 1 1228 | assert close(pid) === :ok 1229 | assert TestIO.binread() === <<>> 1230 | end 1231 | 1232 | test "get_stats :gen_event with no stats" do 1233 | ref = make_ref() 1234 | { :ok, pid } = GE.start_link(fn() -> { :ok, ref } end) 1235 | assert Core.Sys.get_stats(pid) === nil 1236 | assert close(pid) === :ok 1237 | assert TestIO.binread() === <<>> 1238 | end 1239 | 1240 | test "get_stats :gen_fsm with no stats" do 1241 | ref = make_ref() 1242 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref } end) 1243 | assert Core.Sys.get_stats(pid) === nil 1244 | assert close(pid) === :ok 1245 | assert TestIO.binread() === <<>> 1246 | end 1247 | 1248 | test "get_stats :gen_fsm with 1 message in" do 1249 | ref = make_ref() 1250 | { :ok, pid } = GFSM.start_link(fn() -> { :ok, :state, ref} end, 1251 | [:statistics]) 1252 | send(pid, 1) 1253 | stats = Core.Sys.get_stats(pid) 1254 | assert stats[:in] === 1 1255 | assert close(pid) === :ok 1256 | assert TestIO.binread() === <<>> 1257 | end 1258 | 1259 | test "print_stats with one of each event" do 1260 | ref1 = make_ref() 1261 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1262 | [{ :debug, [{ :stats, true }] }]) 1263 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello } }, 500) 1264 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello, self() } }, 500) 1265 | :ok = Core.call(pid, __MODULE__, { :event, { :out, :hello, self() } }, 500) 1266 | assert Core.Sys.print_stats(pid) === :ok 1267 | assert close(pid) === :ok 1268 | output = TestIO.binread() 1269 | pattern = "\\A\\*\\* Core.Debug #{inspect(pid)} statistics:\n" <> 1270 | " Start Time: \\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\n" <> 1271 | " Current Time: \\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\n" <> 1272 | " Messages In: 2\n" <> 1273 | " Messages Out: 1\n" <> 1274 | " Reductions: \\d+\n" <> 1275 | "\n\\z" 1276 | regex = Regex.compile!(pattern) 1277 | assert Regex.match?(regex, output), 1278 | "#{inspect(regex)} not found in #{output}" 1279 | end 1280 | 1281 | test "print_stats with no stats" do 1282 | ref1 = make_ref() 1283 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 1284 | assert Core.Sys.print_stats(pid) === :ok 1285 | assert close(pid) === :ok 1286 | report = "** Core.Debug #{inspect(pid)} statistics not active\n" <> 1287 | "\n" 1288 | assert TestIO.binread() === report 1289 | end 1290 | 1291 | test "set_stats true with a cast message in" do 1292 | ref1 = make_ref() 1293 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end) 1294 | assert Core.Sys.set_stats(pid, true) === :ok 1295 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello } }, 500) 1296 | stats = Core.Sys.get_stats(pid) 1297 | assert stats[:in] === 1 1298 | assert close(pid) === :ok 1299 | assert TestIO.binread() === <<>> 1300 | end 1301 | 1302 | test "set_stats false after a cast message in" do 1303 | ref1 = make_ref() 1304 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1305 | [{ :debug, [{ :stats, true }] }]) 1306 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello } }, 500) 1307 | assert Core.Sys.set_stats(pid, false) === :ok 1308 | assert Core.Sys.get_stats(pid) === nil 1309 | assert close(pid) === :ok 1310 | assert TestIO.binread() === <<>> 1311 | end 1312 | 1313 | test "log_file" do 1314 | ref1 = make_ref() 1315 | file = Path.join(__DIR__, "logfile") 1316 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1317 | [{ :debug, [{ :log_file, file }] }]) 1318 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 1319 | assert Core.Sys.set_log_file(pid, nil) === :ok 1320 | log = "** Core.Debug #{inspect(pid)} #{inspect({ :event, 1 })}\n" 1321 | assert File.read!(file) === log 1322 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 1323 | assert Core.Sys.set_log_file(pid, file) === :ok 1324 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 3 } }, 500) 1325 | assert close(pid) === :ok 1326 | log = "** Core.Debug #{inspect(pid)} #{inspect({ :event, 3 })}\n" 1327 | assert File.read!(file) === log 1328 | assert TestIO.binread() === <<>> 1329 | end 1330 | 1331 | test "log_file bad file" do 1332 | ref1 = make_ref() 1333 | file = Path.join(Path.join(__DIR__, "baddir"), "logfile") 1334 | pid = Core.spawn_link( __MODULE__, fn() -> ref1 end) 1335 | assert_raise ArgumentError, "could not open file: #{inspect(file)}", 1336 | fn() -> Core.Sys.set_log_file(pid, file) end 1337 | assert close(pid) === :ok 1338 | assert TestIO.binread() === <<>> 1339 | end 1340 | 1341 | test "trace" do 1342 | ref1 = make_ref() 1343 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1344 | [{ :debug, [{ :trace, true }] }]) 1345 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 1346 | assert Core.Sys.set_trace(pid, false) === :ok 1347 | report1 = "** Core.Debug #{inspect(pid)} #{inspect({ :event, 1 })}\n" 1348 | assert TestIO.binread() === report1 1349 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 1350 | assert TestIO.binread() === report1 1351 | assert Core.Sys.set_trace(pid, true) === :ok 1352 | :ok = Core.call(pid, __MODULE__, { :event, { :in, :hello, self() } }, 500) 1353 | assert close(pid) === :ok 1354 | report2 = "** Core.Debug #{inspect(pid)} " <> 1355 | "message in (from #{inspect(self())}): :hello\n" 1356 | assert TestIO.binread() === "#{report1}#{report2}" 1357 | end 1358 | 1359 | test "hook" do 1360 | ref1 = make_ref() 1361 | hook = fn(to, event, process) -> send(to, { process, event }) end 1362 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1363 | [{ :debug, [{ :hook, { hook, self() } }] }]) 1364 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 1365 | assert_received { ^pid, { :event, 1 } }, "hook did not send message" 1366 | assert Core.Sys.set_hook(pid, hook, nil) === :ok 1367 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 1368 | refute_received { ^pid, { :event, 2 } }, 1369 | "set_hook nil did not stop hook" 1370 | assert close(pid) === :ok 1371 | assert TestIO.binread() === <<>> 1372 | end 1373 | 1374 | test "hook set_hook changes hook state" do 1375 | ref1 = make_ref() 1376 | hook = fn(to, event, process) -> send(to, { process, event }) end 1377 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1378 | [{ :debug, [{ :hook, { hook, self() } }] }]) 1379 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 1380 | assert_received { ^pid, { :event, 1 } }, "hook did not send message" 1381 | assert Core.Sys.set_hook(pid, hook, pid) 1382 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 1383 | refute_received { ^pid, { :event, 2 } }, 1384 | "strt_hook did not change hook state" 1385 | assert close(pid) === :ok 1386 | assert TestIO.binread() === <<>> 1387 | end 1388 | 1389 | test "hook raises" do 1390 | ref1 = make_ref() 1391 | hook = fn(_to, :raise, _process) -> 1392 | raise(ArgumentError, []) 1393 | (to, event, process) -> 1394 | send(to, { process, event }) 1395 | end 1396 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1397 | [{ :debug, [{ :hook, { hook, self() } }] }]) 1398 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 1399 | assert_received { ^pid, { :event, 1 } }, "hook did not send message" 1400 | :ok = Core.call(pid, __MODULE__, { :event, :raise }, 500) 1401 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 1402 | refute_received { ^pid, { :event, 2 } }, "hook raise did not stop it" 1403 | assert close(pid) === :ok 1404 | assert TestIO.binread() === <<>> 1405 | end 1406 | 1407 | test "hook done" do 1408 | ref1 = make_ref() 1409 | hook = fn(_to, :done, _process) -> 1410 | :done 1411 | (to, event, process) -> 1412 | send(to, { process, event }) 1413 | end 1414 | pid = Core.spawn_link(__MODULE__, fn() -> ref1 end, 1415 | [{ :debug, [{ :hook, { hook, self() } }] }]) 1416 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 1 } }, 500) 1417 | assert_received { ^pid, { :event, 1 } }, "hook did not send message" 1418 | :ok = Core.call(pid, __MODULE__, { :event, :done }, 500) 1419 | :ok = Core.call(pid, __MODULE__, { :event, { :event, 2 } }, 500) 1420 | refute_received { ^pid, { :event, 2 } }, "hook done did not stop it" 1421 | assert close(pid) === :ok 1422 | assert TestIO.binread() === <<>> 1423 | end 1424 | 1425 | ## utils 1426 | 1427 | defp close(pid) do 1428 | Process.unlink(pid) 1429 | ref = Process.monitor(pid) 1430 | Process.exit(pid, :shutdown) 1431 | receive do 1432 | { :DOWN, ^ref, _, _, :shutdown } -> 1433 | :ok 1434 | after 1435 | 500 -> 1436 | Process.demonitor(ref, [:flush]) 1437 | Process.link(pid) 1438 | :timeout 1439 | end 1440 | end 1441 | 1442 | defp inspect_erl(term) do 1443 | :io_lib.format('~p', [term]) 1444 | |> List.flatten() 1445 | |> List.to_string() 1446 | end 1447 | 1448 | end 1449 | -------------------------------------------------------------------------------- /test/core_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "test_helper.exs", __DIR__ 2 | 3 | defmodule CoreTest do 4 | use ExUnit.Case 5 | 6 | use Core 7 | require Core.Debug 8 | 9 | def init(parent, debug, fun) do 10 | fun.(parent, debug) 11 | end 12 | 13 | def continue(fun, parent, debug) do 14 | fun.(parent, debug) 15 | end 16 | 17 | setup_all do 18 | TestIO.setup_all() 19 | end 20 | 21 | setup do 22 | TestIO.setup() 23 | end 24 | 25 | test "start_link with init_ack/0" do 26 | starter = self() 27 | fun = fn(parent, _debug) -> 28 | Core.init_ack() 29 | send(starter, { :parent, parent }) 30 | close() 31 | end 32 | assert { :ok, pid } = Core.start_link(__MODULE__, fun) 33 | assert_receive { :parent, ^starter }, 200, "parent not starter" 34 | assert linked?(pid), "child not linked" 35 | assert :normal === close(pid), "close failed" 36 | assert TestIO.binread() === <<>> 37 | end 38 | 39 | test "start_link with timeout" do 40 | fun = fn(_parent, _debug) -> 41 | :timer.sleep(5000) 42 | end 43 | assert { :error, :timeout } = Core.start_link(__MODULE__, fun, 44 | [{ :timeout, 1 }]), "didn't timeout" 45 | assert TestIO.binread() === <<>> 46 | end 47 | 48 | test "start_link with init_ignore" do 49 | fun = fn(_parent, _debug) -> 50 | Core.init_ignore() 51 | :timer.sleep(5000) 52 | end 53 | trap = Process.flag(:trap_exit, true) 54 | assert :ignore === Core.start_link(__MODULE__, fun) 55 | assert_receive { :EXIT, _child, :normal }, 200, "init_ignore did not exit" 56 | Process.flag(:trap_exit, trap) 57 | assert TestIO.binread() === <<>> 58 | end 59 | 60 | test "start_link with init_ack/1" do 61 | fun = fn(_parent, _debug) -> 62 | Core.init_ack(:extra_data) 63 | close() 64 | end 65 | assert { :ok, pid, :extra_data } = Core.start_link(__MODULE__, fun) 66 | assert close(pid) === :normal, "close failed" 67 | assert TestIO.binread() === <<>> 68 | end 69 | 70 | test "start_link with init_stop" do 71 | fun = fn(parent, debug) -> 72 | Core.init_stop(__MODULE__, parent, debug, nil, :init_error) 73 | :timer.sleep(5000) 74 | end 75 | trap = Process.flag(:trap_exit, :true) 76 | assert { :error, :init_error } = Core.start_link(__MODULE__, fun) 77 | assert_receive { :EXIT, _pid, :init_error }, 200, "init_stop did not exit" 78 | Process.flag(:trap_exit, trap) 79 | assert TestIO.binread() === <<>> 80 | end 81 | 82 | test "start_link register local name" do 83 | fun = fn(_parent, _debug) -> 84 | Core.init_ack() 85 | close() 86 | end 87 | opts = [local: :core_start_link] 88 | assert { :ok, pid1 } = Core.start_link(__MODULE__, fun, opts) 89 | assert Core.whereis(:core_start_link) === pid1 90 | trap = Process.flag(:trap_exit, true) 91 | assert { :error, { :already_started, ^pid1 } } = Core.start_link(__MODULE__, 92 | fun, opts) 93 | assert_receive { :EXIT, _pid2, :normal }, 200, 94 | "already started did not exit normally" 95 | Process.flag(:trap_exit, trap) 96 | assert close(pid1) === :normal 97 | assert TestIO.binread() === <<>> 98 | end 99 | 100 | test "start_link register global name" do 101 | fun = fn(_parent, _debug) -> 102 | Core.init_ack() 103 | close() 104 | end 105 | opts = [global: :core_start_link] 106 | assert { :ok, pid1 } = Core.start_link(__MODULE__, fun, opts) 107 | assert Core.whereis({ :global, :core_start_link }) === pid1 108 | trap = Process.flag(:trap_exit, true) 109 | assert { :error, { :already_started, ^pid1 } } = Core.start_link(__MODULE__, 110 | fun, opts) 111 | assert_receive { :EXIT, _pid2, :normal }, 200, 112 | "already started did not exit normally" 113 | Process.flag(:trap_exit, trap) 114 | assert close(pid1) === :normal 115 | assert TestIO.binread() === <<>> 116 | end 117 | 118 | test "start_link register via name" do 119 | fun = fn(_parent, _debug) -> 120 | Core.init_ack() 121 | close() 122 | end 123 | opts = [{ :via, { :global, :core_start_link_via } }] 124 | assert { :ok, pid1 } = Core.start_link(__MODULE__, fun, opts) 125 | assert Core.whereis({ :via, :global, :core_start_link_via }) === pid1 126 | trap = Process.flag(:trap_exit, true) 127 | assert { :error, { :already_started, ^pid1 } } = Core.start_link(__MODULE__, 128 | fun, opts) 129 | assert_receive { :EXIT, _pid2, :normal }, 200, 130 | "already started did not exit normally" 131 | Process.flag(:trap_exit, trap) 132 | assert close(pid1) === :normal 133 | assert TestIO.binread() === <<>> 134 | end 135 | 136 | test "start with init_ack/0" do 137 | starter = self() 138 | fun = fn(parent, _debug) -> 139 | Core.init_ack() 140 | send(starter, { :parent, parent }) 141 | close() 142 | end 143 | { :ok, pid } = Core.start(__MODULE__, fun) 144 | assert_receive { :parent, ^pid }, 200, "parent not self()" 145 | refute linked?(pid), "child linked" 146 | assert close(pid) === :normal, "close failed" 147 | assert TestIO.binread() === <<>> 148 | end 149 | 150 | test "start with timeout" do 151 | fun = fn(_parent, _debug) -> 152 | :timer.sleep(5000) 153 | end 154 | assert { :error, :timeout } = Core.start(__MODULE__, fun, 155 | [{ :timeout, 1 }]), "didn't timeout" 156 | assert TestIO.binread() === <<>> 157 | end 158 | 159 | 160 | test "start register local name" do 161 | fun = fn(_parent, _debug) -> 162 | Core.init_ack() 163 | close() 164 | end 165 | opts = [local: :core_start] 166 | assert { :ok, pid1 } = Core.start(__MODULE__, fun, opts) 167 | assert Core.whereis(:core_start) === pid1 168 | assert { :error, { :already_started, ^pid1 } } = Core.start_link(__MODULE__, 169 | fun, opts) 170 | assert close(pid1) === :normal 171 | assert TestIO.binread() === <<>> 172 | end 173 | 174 | test "start with link" do 175 | starter = self() 176 | fun = fn(parent, _debug) -> 177 | send(starter, { :parent, parent }) 178 | Core.init_ack() 179 | close() 180 | end 181 | { :ok, pid } = Core.start(__MODULE__, fun, [{ :spawn_opt, [:link] }]) 182 | assert_received { :parent, ^pid }, "parent not self()" 183 | assert linked?(pid), "child not linked" 184 | assert :normal === close(pid), "close failed" 185 | assert TestIO.binread() === <<>> 186 | end 187 | 188 | test "spawn_link with init_ack/0" do 189 | starter = self() 190 | fun = fn(parent, _debug) -> 191 | Core.init_ack() 192 | send(starter, { :parent, parent }) 193 | close() 194 | end 195 | pid = Core.spawn_link(__MODULE__, fun) 196 | assert_receive { :parent, ^starter }, 200, "parent not starter" 197 | assert linked?(pid), "child not linked" 198 | assert close(pid) === :normal, "close failed" 199 | refute_received { :ack, ^pid, _ }, "init_ack sent" 200 | assert TestIO.binread() === <<>> 201 | end 202 | 203 | test "spawn_link with init_ignore" do 204 | fun = fn(_parent, _debug) -> 205 | Core.init_ignore() 206 | :timer.sleep(5000) 207 | end 208 | trap = Process.flag(:trap_exit, true) 209 | pid = Core.spawn_link(__MODULE__, fun) 210 | assert_receive { :EXIT, ^pid, :normal }, 200, "init_ignore did not exit" 211 | Process.flag(:trap_exit, trap) 212 | refute_received { :ack, ^pid, _ }, "init_ack sent" 213 | assert TestIO.binread() === <<>> 214 | end 215 | 216 | test "spawn_link with init_ack/1" do 217 | fun = fn(_parent, _debug) -> 218 | Core.init_ack(:extra_data) 219 | close() 220 | end 221 | pid = Core.spawn_link(__MODULE__, fun) 222 | assert close(pid) === :normal, "close failed" 223 | refute_received { :ack, ^pid, _ }, "init_ack sent" 224 | assert TestIO.binread() === <<>> 225 | end 226 | 227 | test "spawn_link with init_stop" do 228 | fun = fn(parent, debug) -> 229 | Core.init_stop(__MODULE__, parent, debug, nil, :init_error) 230 | :timer.sleep(5000) 231 | end 232 | trap = Process.flag(:trap_exit, :true) 233 | pid = Core.spawn_link(__MODULE__, fun) 234 | assert_receive { :EXIT, ^pid, :init_error }, 200, "init_stop did not exit" 235 | Process.flag(:trap_exit, trap) 236 | refute_received { :ack, ^pid, _ }, "init_ack sent" 237 | report = "** #{erl_inspect(__MODULE__)} #{erl_inspect(pid)} is terminating\n" <> 238 | "** Last event was #{erl_inspect(nil)}\n" <> 239 | "** Arguments == #{erl_inspect(nil)}\n" <> 240 | "** Process == #{erl_inspect(pid)}\n" <> 241 | "** Parent == #{erl_inspect(self())}\n" <> 242 | "** Reason for termination == \n"<> 243 | "#{List.to_string(:io_lib.format('** ~p', [:init_error]))}\n\n" 244 | assert TestIO.binread() === report 245 | end 246 | 247 | test "spawn_link with init_stop and debug" do 248 | fun = fn(parent, debug) -> 249 | event = :test_event 250 | debug = Core.Debug.event(debug, event) 251 | Core.init_stop(__MODULE__, parent, debug, nil, :init_error, event) 252 | :timer.sleep(5000) 253 | end 254 | trap = Process.flag(:trap_exit, :true) 255 | pid = Core.spawn_link(__MODULE__, fun, [{ :debug, [{ :log, 10 }] }]) 256 | assert_receive { :EXIT, ^pid, reason }, 200, "init_stop did not exit" 257 | assert reason === :init_error 258 | Process.flag(:trap_exit, trap) 259 | refute_received { :ack, ^pid, _ }, "init_ack sent" 260 | output = TestIO.binread() 261 | report = "** #{erl_inspect(__MODULE__)} #{erl_inspect(pid)} is terminating\n" <> 262 | "** Last event was #{erl_inspect(:test_event)}\n" <> 263 | "** Arguments == #{erl_inspect(nil)}\n" <> 264 | "** Process == #{erl_inspect(pid)}\n" <> 265 | "** Parent == #{erl_inspect(self())}\n" <> 266 | "** Reason for termination == \n"<> 267 | "#{List.to_string(:io_lib.format('** ~p', [reason]))}\n\n" 268 | assert String.contains?(output, [report]) 269 | log = "** Core.Debug #{inspect(pid)} event log:\n" <> 270 | "** Core.Debug #{inspect(pid)} :test_event\n\n" 271 | assert String.contains?(output, [log]) 272 | assert byte_size(output) === (byte_size(report) + byte_size(log)) 273 | "unexpected output in:\n #{output}" 274 | end 275 | 276 | test "spawn_link with normal init_stop and debug" do 277 | fun = fn(parent, debug) -> 278 | event = :test_event 279 | debug = Core.Debug.event(debug, event) 280 | Core.init_stop(__MODULE__, parent, debug, nil, :normal, event) 281 | :timer.sleep(5000) 282 | end 283 | trap = Process.flag(:trap_exit, :true) 284 | pid = Core.spawn_link(__MODULE__, fun, [{ :debug, [{ :log, 10 }] }]) 285 | assert_receive { :EXIT, ^pid, reason }, 200, "init_stop did not exit" 286 | assert reason === :normal 287 | Process.flag(:trap_exit, trap) 288 | refute_received { :ack, ^pid, _ }, "init_ack sent" 289 | assert TestIO.binread() === <<>> 290 | end 291 | 292 | test "spawn_link with shutdown init_stop and debug" do 293 | fun = fn(parent, debug) -> 294 | event = :test_event 295 | debug = Core.Debug.event(debug, event) 296 | Core.init_stop(__MODULE__, parent, debug, nil, :shutdown, event) 297 | :timer.sleep(5000) 298 | end 299 | trap = Process.flag(:trap_exit, :true) 300 | pid = Core.spawn_link(__MODULE__, fun, [{ :debug, [{ :log, 10 }] }]) 301 | assert_receive { :EXIT, ^pid, reason }, 200, "init_stop did not exit" 302 | assert reason === :shutdown 303 | Process.flag(:trap_exit, trap) 304 | refute_received { :ack, ^pid, _ }, "init_ack sent" 305 | assert TestIO.binread() === <<>> 306 | end 307 | 308 | test "spawn_link with shutdown term init_stop and debug" do 309 | fun = fn(parent, debug) -> 310 | event = :test_event 311 | debug = Core.Debug.event(debug, event) 312 | Core.init_stop(__MODULE__, parent, debug, nil, { :shutdown, nil }, event) 313 | :timer.sleep(5000) 314 | end 315 | trap = Process.flag(:trap_exit, :true) 316 | pid = Core.spawn_link(__MODULE__, fun, [{ :debug, [{ :log, 10 }] }]) 317 | assert_receive { :EXIT, ^pid, reason }, 200, "init_stop did not exit" 318 | assert reason === { :shutdown, nil } 319 | Process.flag(:trap_exit, trap) 320 | refute_received { :ack, ^pid, _ }, "init_ack sent" 321 | assert TestIO.binread() === <<>> 322 | end 323 | 324 | test "spawn_link register local name" do 325 | starter = self() 326 | fun = fn(_parent, _debug) -> 327 | send(starter, { :registered, self() }) 328 | Core.init_ack() 329 | close() 330 | end 331 | name = :core_spawn_link 332 | pid1 = Core.spawn_link(__MODULE__, fun, local: name) 333 | assert_receive { :registered, ^pid1 }, 200, "did not register" 334 | assert Core.whereis(name) === pid1 335 | pid2 = Core.spawn_link(__MODULE__, fun, local: name) 336 | ref = Process.monitor(pid2) 337 | assert_receive { :DOWN, ^ref, _, _, :normal }, 200, 338 | "already registered did not exit with reason :normal" 339 | refute_received { :registered, ^pid2 }, 340 | "already started did not prevent init" 341 | refute_received { :ack, ^pid2, _ }, "already started sent init_ack" 342 | assert close(pid1) === :normal 343 | assert TestIO.binread() === <<>> 344 | end 345 | 346 | test "spawn with init_ack/0" do 347 | starter = self() 348 | fun = fn(parent, _debug) -> 349 | Core.init_ack() 350 | send(starter, { :parent, parent }) 351 | close() 352 | end 353 | pid = Core.spawn(__MODULE__, fun) 354 | assert_receive { :parent, ^pid }, 200, "parent not self()" 355 | refute linked?(pid), "child linked" 356 | assert close(pid) === :normal, "close failed" 357 | refute_received { :ack, ^pid, _ }, "init_ack sent" 358 | assert TestIO.binread() === <<>> 359 | end 360 | 361 | test "spawn register local name" do 362 | starter = self() 363 | fun = fn(_parent, _debug) -> 364 | send(starter, { :registered, self() }) 365 | Core.init_ack() 366 | close() 367 | end 368 | name = :core_spawn 369 | pid1 = Core.spawn(__MODULE__, fun, local: name) 370 | assert_receive { :registered, ^pid1 }, 200, "did not register" 371 | assert Core.whereis(name) === pid1 372 | pid2 = Core.spawn(__MODULE__, fun, local: name) 373 | ref = Process.monitor(pid2) 374 | assert_receive { :DOWN, ^ref, _, _, :normal }, 200, 375 | "already registered did not exit with reason :normal" 376 | refute_received { :registered, ^pid2 }, 377 | "already started did not prevent init" 378 | refute_received { :ack, ^pid2, _ }, "already started sent init_ack" 379 | assert close(pid1) === :normal 380 | assert TestIO.binread() === <<>> 381 | end 382 | 383 | test "spawn with link" do 384 | fun = fn(_parent, _debug) -> 385 | Core.init_ack() 386 | close() 387 | end 388 | pid = Core.spawn(__MODULE__, fun, [{ :spawn_opt, [:link] }]) 389 | assert linked?(pid), "child not linked" 390 | assert :normal === close(pid), "close failed" 391 | assert TestIO.binread() === <<>> 392 | end 393 | 394 | test "stop" do 395 | fun = fn(parent, debug) -> 396 | Core.init_ack() 397 | Core.stop(__MODULE__, nil, parent, debug, :error) 398 | :timer.sleep(5000) 399 | end 400 | trap = Process.flag(:trap_exit, :true) 401 | pid = Core.spawn_link(__MODULE__, fun) 402 | assert_receive { :EXIT, ^pid, :error }, 200, "stop did not exit" 403 | Process.flag(:trap_exit, trap) 404 | report = "** #{erl_inspect(__MODULE__)} #{erl_inspect(pid)} is terminating\n" <> 405 | "** Last event was #{erl_inspect(nil)}\n"<> 406 | "** State == #{erl_inspect(nil)}\n" <> 407 | "** Process == #{erl_inspect(pid)}\n" <> 408 | "** Parent == #{erl_inspect(self())}\n" <> 409 | "** Reason for termination == \n"<> 410 | "#{List.to_string(:io_lib.format('** ~p', [:error]))}\n\n" 411 | assert TestIO.binread() === report 412 | end 413 | 414 | test "stop and debug" do 415 | fun = fn(parent, debug) -> 416 | Core.init_ack() 417 | event = :test_event 418 | debug = Core.Debug.event(debug, event) 419 | Core.stop(__MODULE__, nil, parent, debug, :error, event) 420 | :timer.sleep(5000) 421 | end 422 | trap = Process.flag(:trap_exit, :true) 423 | pid = Core.spawn_link(__MODULE__, fun, [{ :debug, [{ :log, 10 }] }]) 424 | assert_receive { :EXIT, ^pid, reason }, 200, "stop did not exit" 425 | assert reason === :error 426 | Process.flag(:trap_exit, trap) 427 | output = TestIO.binread() 428 | report = "** #{erl_inspect(__MODULE__)} #{erl_inspect(pid)} is terminating\n" <> 429 | "** Last event was #{erl_inspect(:test_event)}\n"<> 430 | "** State == #{erl_inspect(nil)}\n" <> 431 | "** Process == #{erl_inspect(pid)}\n" <> 432 | "** Parent == #{erl_inspect(self())}\n" <> 433 | "** Reason for termination == \n"<> 434 | "#{List.to_string(:io_lib.format('** ~p', [reason]))}\n\n" 435 | assert String.contains?(output, [report]) 436 | log = "** Core.Debug #{inspect(pid)} event log:\n" <> 437 | "** Core.Debug #{inspect(pid)} :test_event\n\n" 438 | assert String.contains?(output, [log]) 439 | assert byte_size(output) === (byte_size(report) + byte_size(log)) 440 | "unexpected output in:\n #{output}" 441 | end 442 | 443 | test "stop with normal and debug" do 444 | fun = fn(parent, debug) -> 445 | Core.init_ack() 446 | event = :test_event 447 | debug = Core.Debug.event(debug, event) 448 | Core.stop(__MODULE__, nil, parent, debug, :normal, event) 449 | :timer.sleep(5000) 450 | end 451 | trap = Process.flag(:trap_exit, :true) 452 | pid = Core.spawn_link(__MODULE__, fun, [{ :debug, [{ :log, 10 }] }]) 453 | assert_receive { :EXIT, ^pid, reason }, 200, "stop did not exit" 454 | assert reason === :normal 455 | Process.flag(:trap_exit, trap) 456 | assert TestIO.binread() === <<>> 457 | end 458 | 459 | test "stop with shutdown and debug" do 460 | fun = fn(parent, debug) -> 461 | Core.init_ack() 462 | event = :test_event 463 | debug = Core.Debug.event(debug, event) 464 | Core.stop(__MODULE__, nil, parent, debug, :shutdown, event) 465 | :timer.sleep(5000) 466 | end 467 | trap = Process.flag(:trap_exit, :true) 468 | pid = Core.spawn_link(__MODULE__, fun, [{ :debug, [{ :log, 10 }] }]) 469 | assert_receive { :EXIT, ^pid, reason }, 200, "stop did not exit" 470 | assert reason === :shutdown 471 | Process.flag(:trap_exit, trap) 472 | assert TestIO.binread() === <<>> 473 | end 474 | 475 | test "stop with shutdown term and debug" do 476 | fun = fn(parent, debug) -> 477 | Core.init_ack() 478 | event = :test_event 479 | debug = Core.Debug.event(debug, event) 480 | Core.stop(__MODULE__, nil, parent, debug, { :shutdown, nil }, event) 481 | :timer.sleep(5000) 482 | end 483 | trap = Process.flag(:trap_exit, :true) 484 | pid = Core.spawn_link(__MODULE__, fun, [{ :debug, [{ :log, 10 }] }]) 485 | assert_receive { :EXIT, ^pid, reason }, 200, "stop did not exit" 486 | assert reason === { :shutdown, nil } 487 | Process.flag(:trap_exit, trap) 488 | assert TestIO.binread() === <<>> 489 | end 490 | 491 | test "uncaught exception" do 492 | exception = ArgumentError.exception([message: "hello"]) 493 | fun = fn(_parent, _debug) -> 494 | Core.init_ack() 495 | raise exception 496 | end 497 | trap = Process.flag(:trap_exit, :true) 498 | pid = Core.spawn_link(__MODULE__, fun) 499 | assert_receive { :EXIT, ^pid, reason }, 200, "child did not exit" 500 | assert { ^exception, stack } = reason 501 | assert { __MODULE__, _, _, _ } = hd(stack), 502 | "stacktrace not from __MODULE__" 503 | Process.flag(:trap_exit, trap) 504 | report = "** Core #{erl_inspect(pid)} is terminating\n" <> 505 | "** Module == #{erl_inspect(__MODULE__)}\n" <> 506 | "** Process == #{erl_inspect(pid)}\n" <> 507 | "** Parent == #{erl_inspect(self())}\n" <> 508 | "** Reason for termination == \n" <> 509 | "#{List.to_string(:io_lib.format('** ~p', [reason]))}\n\n" 510 | assert TestIO.binread() === report 511 | end 512 | 513 | test "uncaught throw" do 514 | fun = fn(_parent, _debug) -> 515 | Core.init_ack() 516 | throw(:thrown) 517 | end 518 | trap = Process.flag(:trap_exit, :true) 519 | pid = Core.spawn_link(__MODULE__, fun) 520 | assert_receive { :EXIT, ^pid, reason }, 200, "child did not exit" 521 | assert { { :nocatch, :thrown }, stack } = reason 522 | assert { __MODULE__, _, _, _ } = hd(stack), 523 | "stacktrace not from __MODULE__" 524 | Process.flag(:trap_exit, trap) 525 | report = "** Core #{erl_inspect(pid)} is terminating\n" <> 526 | "** Module == #{erl_inspect(__MODULE__)}\n" <> 527 | "** Process == #{erl_inspect(pid)}\n" <> 528 | "** Parent == #{erl_inspect(self())}\n" <> 529 | "** Reason for termination == \n"<> 530 | "#{List.to_string(:io_lib.format('** ~p', [reason]))}\n\n" 531 | assert TestIO.binread() === report 532 | end 533 | 534 | test "uncaught exit" do 535 | fun = fn(_parent, _debug) -> 536 | Core.init_ack() 537 | exit(:exited) 538 | end 539 | trap = Process.flag(:trap_exit, :true) 540 | pid = Core.spawn_link(__MODULE__, fun) 541 | assert_receive { :EXIT, ^pid, reason }, 200, "child did not exit" 542 | assert :exited = reason, "reason is not exited" 543 | Process.flag(:trap_exit, trap) 544 | assert TestIO.binread() === <<>> 545 | end 546 | 547 | test "uncaught exception after hibernate" do 548 | exception = ArgumentError.exception([message: "hello"]) 549 | fun = fn(parent, debug) -> 550 | Core.init_ack() 551 | # send message to self to wake up immediately 552 | send(self(), :awaken) 553 | fun2 = fn(_parent, _debug) -> raise ArgumentError, [message: "hello"] end 554 | Core.hibernate(__MODULE__, :continue, fun2, parent, debug) 555 | end 556 | trap = Process.flag(:trap_exit, :true) 557 | pid = Core.spawn_link(__MODULE__, fun) 558 | assert_receive { :EXIT, ^pid, reason }, 200, "child did not exit" 559 | assert { ^exception, stack } = reason 560 | assert { __MODULE__, _, _, _ } = hd(stack), 561 | "stacktrace not from __MODULE__" 562 | Process.flag(:trap_exit, trap) 563 | report = "** Core #{erl_inspect(pid)} is terminating\n" <> 564 | "** Module == #{erl_inspect(__MODULE__)}\n" <> 565 | "** Process == #{erl_inspect(pid)}\n" <> 566 | "** Parent == #{erl_inspect(self())}\n" <> 567 | "** Reason for termination == \n"<> 568 | "#{List.to_string(:io_lib.format('** ~p', [reason]))}\n\n" 569 | assert TestIO.binread() === report 570 | end 571 | 572 | test "uncaught throw after hibernate" do 573 | fun = fn(parent, debug) -> 574 | Core.init_ack() 575 | # send message to self to wake up immediately 576 | send(self(), :awaken) 577 | fun2 = fn(_parent, _debug) -> throw(:thrown) end 578 | Core.hibernate(__MODULE__, :continue, fun2, parent, debug) 579 | end 580 | trap = Process.flag(:trap_exit, :true) 581 | pid = Core.spawn_link(__MODULE__, fun) 582 | assert_receive { :EXIT, ^pid, reason }, 200, "child did not exit" 583 | assert { { :nocatch, :thrown }, stack } = reason 584 | assert { __MODULE__, _, _, _ } = hd(stack), 585 | "stacktrace not from __MODULE__" 586 | Process.flag(:trap_exit, trap) 587 | report = "** Core #{erl_inspect(pid)} is terminating\n" <> 588 | "** Module == #{erl_inspect(__MODULE__)}\n" <> 589 | "** Process == #{erl_inspect(pid)}\n" <> 590 | "** Parent == #{erl_inspect(self())}\n" <> 591 | "** Reason for termination == \n"<> 592 | "#{List.to_string(:io_lib.format('** ~p', [reason]))}\n\n" 593 | assert TestIO.binread() === report 594 | end 595 | 596 | test "uncaught exit after hibernate" do 597 | fun = fn(parent, debug) -> 598 | Core.init_ack() 599 | # send message to self to wake up immediately 600 | send(self(), :awaken) 601 | fun2 = fn(_parent, _debug) -> exit(:exited) end 602 | Core.hibernate(__MODULE__, :continue, fun2, parent, debug) 603 | end 604 | trap = Process.flag(:trap_exit, :true) 605 | pid = Core.spawn_link(__MODULE__, fun) 606 | assert_receive { :EXIT, ^pid, reason }, 200, "child did not exit" 607 | assert :exited = reason, "reason is not exited" 608 | Process.flag(:trap_exit, trap) 609 | assert TestIO.binread() === <<>> 610 | end 611 | 612 | test "call to pid" do 613 | fun = fn(_parent, _debug) -> 614 | Core.init_ack() 615 | receive do 616 | { __MODULE__, from, :hello } -> 617 | Core.reply(from, :hi) 618 | end 619 | close() 620 | end 621 | pid = Core.spawn_link(__MODULE__, fun) 622 | assert :hi === Core.call(pid, __MODULE__, :hello, 1000) 623 | close(pid) 624 | assert TestIO.binread() === <<>> 625 | end 626 | 627 | test "call to local name" do 628 | fun = fn(_parent, _debug) -> 629 | Core.init_ack() 630 | receive do 631 | { __MODULE__, from, :hello } -> 632 | Core.reply(from, :hi) 633 | end 634 | close() 635 | end 636 | { :ok, pid } = Core.start_link(__MODULE__, fun, local: :call_local) 637 | assert :hi === Core.call(:call_local, __MODULE__, :hello, 1000) 638 | close(pid) 639 | assert TestIO.binread() === <<>> 640 | end 641 | 642 | test "call to local name with local node" do 643 | fun = fn(_parent, _debug) -> 644 | Core.init_ack() 645 | receive do 646 | { __MODULE__, from, :hello } -> 647 | Core.reply(from, :hi) 648 | end 649 | close() 650 | end 651 | { :ok, pid } = Core.start_link(__MODULE__, fun, local: :call_local2) 652 | assert :hi === Core.call({ :call_local2, node() }, __MODULE__, :hello, 1000) 653 | close(pid) 654 | assert TestIO.binread() === <<>> 655 | end 656 | 657 | test "call to global name" do 658 | fun = fn(_parent, _debug) -> 659 | Core.init_ack() 660 | receive do 661 | { __MODULE__, from, :hello } -> 662 | Core.reply(from, :hi) 663 | end 664 | close() 665 | end 666 | name = { :global, :call_global } 667 | { :ok, pid } = Core.start_link(__MODULE__, fun, [name]) 668 | assert :hi === Core.call(name, __MODULE__, :hello, 1000) 669 | close(pid) 670 | assert TestIO.binread() === <<>> 671 | end 672 | 673 | test "call to via name" do 674 | fun = fn(_parent, _debug) -> 675 | Core.init_ack() 676 | receive do 677 | { __MODULE__, from, :hello } -> 678 | Core.reply(from, :hi) 679 | end 680 | close() 681 | end 682 | name = { :via, :global, :call_via } 683 | { :ok, pid } = Core.start_link(__MODULE__, fun, via: { :global, :call_via }) 684 | assert :hi === Core.call(name, __MODULE__, :hello, 1000) 685 | close(pid) 686 | assert TestIO.binread() === <<>> 687 | end 688 | 689 | test "call to pid and timeout" do 690 | fun = fn(_parent, _debug) -> 691 | Core.init_ack() 692 | receive do 693 | { __MODULE__, _from, :hello } -> 694 | nil 695 | end 696 | close() 697 | end 698 | pid = Core.spawn_link(__MODULE__, fun) 699 | assert catch_exit(Core.call(pid, __MODULE__, :hello, 100)) 700 | == {:timeout, {Core, :call, [pid, __MODULE__, :hello, 100]}} 701 | close(pid) 702 | assert TestIO.binread() === <<>> 703 | end 704 | 705 | test "call to pid that is already dead" do 706 | fun = fn(_parent, _debug) -> 707 | Core.init_ack() 708 | close() 709 | end 710 | pid = Core.spawn_link(__MODULE__, fun) 711 | close(pid) 712 | assert catch_exit(Core.call(pid, __MODULE__, :hello, 500)) 713 | == {:noproc, {Core, :call, [pid, __MODULE__, :hello, 500]}} 714 | assert TestIO.binread() === <<>> 715 | end 716 | 717 | test "call to pid that exits" do 718 | fun = fn(_parent, _debug) -> 719 | Core.init_ack() 720 | receive do 721 | { __MODULE__, _from, :hello } -> 722 | exit(:normal) 723 | end 724 | end 725 | pid = Core.spawn_link(__MODULE__, fun) 726 | assert catch_exit(Core.call(pid, __MODULE__, :hello, 500)) 727 | == {:normal, {Core, :call, [pid, __MODULE__, :hello, 500]}} 728 | close(pid) 729 | assert TestIO.binread() === <<>> 730 | end 731 | 732 | test "call to local name that is not registered" do 733 | assert catch_exit(Core.call(:call_bad_local, __MODULE__, :hello, 500)) 734 | == {:noproc, {Core, :call, [:call_bad_local, __MODULE__, :hello, 500]}} 735 | end 736 | 737 | test "call to local name with local node that is not registered" do 738 | assert catch_exit(Core.call({:call_bad_local, node()}, __MODULE__, :hello, 500)) 739 | == {:noproc, 740 | {Core, :call, [{:call_bad_local, node()}, __MODULE__, :hello, 500]}} 741 | end 742 | 743 | test "call to global name that is not registered" do 744 | assert catch_exit(Core.call({:global, :call_bad_global}, __MODULE__, :hello, 500)) 745 | == {:noproc, 746 | {Core, :call, [{:global, :call_bad_global}, __MODULE__, :hello, 500]}} 747 | end 748 | 749 | test "call to via name that is not registered" do 750 | assert catch_exit(Core.call({:via, :global, :call_bad_via}, __MODULE__, :hello, 500)) 751 | == {:noproc, 752 | {Core, :call, [{:via, :global, :call_bad_via}, __MODULE__, :hello, 500]}} 753 | end 754 | 755 | test "call to local name on bad node" do 756 | assert catch_exit(Core.call({:call_bad_local, :node_does_not_exist}, __MODULE__, :hello, 500)) 757 | == {{:nodedown, :node_does_not_exist}, 758 | {Core, :call, [{:call_bad_local, :node_does_not_exist}, __MODULE__, :hello, 500]}} 759 | end 760 | 761 | test "cast to pid" do 762 | fun = fn(parent, _debug) -> 763 | Core.init_ack() 764 | receive do 765 | { __MODULE__, :hello } -> 766 | send(parent, { :hi, self() }) 767 | end 768 | close() 769 | end 770 | pid = Core.spawn_link(__MODULE__, fun) 771 | assert :ok === Core.cast(pid, __MODULE__, :hello) 772 | assert_receive { :hi, ^pid }, 200, "cast was not received" 773 | close(pid) 774 | assert TestIO.binread() === <<>> 775 | end 776 | 777 | test "cast to local name" do 778 | fun = fn(parent, _debug) -> 779 | Core.init_ack() 780 | receive do 781 | { __MODULE__, :hello } -> 782 | send(parent, { :hi, self() }) 783 | end 784 | close() 785 | end 786 | { :ok, pid } = Core.start_link(__MODULE__, fun, local: :cast_local) 787 | assert :ok === Core.cast(:cast_local, __MODULE__, :hello) 788 | assert_receive { :hi, ^pid }, 200, "cast was not received" 789 | close(pid) 790 | assert TestIO.binread() === <<>> 791 | end 792 | 793 | test "cast to local name with local node" do 794 | fun = fn(parent, _debug) -> 795 | Core.init_ack() 796 | receive do 797 | { __MODULE__, :hello } -> 798 | send(parent, { :hi, self() }) 799 | end 800 | close() 801 | end 802 | { :ok, pid } = Core.start_link(__MODULE__, fun, local: :cast_local2) 803 | assert :ok === Core.cast({ :cast_local2, node() }, __MODULE__, :hello) 804 | assert_receive { :hi, ^pid }, 200, "cast was not received" 805 | close(pid) 806 | assert TestIO.binread() === <<>> 807 | end 808 | 809 | test "cast to global name" do 810 | fun = fn(parent, _debug) -> 811 | Core.init_ack() 812 | receive do 813 | { __MODULE__, :hello } -> 814 | send(parent, { :hi, self() }) 815 | end 816 | close() 817 | end 818 | name = { :global, :cast_global } 819 | { :ok, pid } = Core.start_link(__MODULE__, fun, [name]) 820 | assert :ok === Core.cast(name, __MODULE__, :hello) 821 | assert_receive { :hi, ^pid }, 200, "cast was not received" 822 | close(pid) 823 | assert TestIO.binread() === <<>> 824 | end 825 | 826 | test "cast to via name" do 827 | fun = fn(parent, _debug) -> 828 | Core.init_ack() 829 | receive do 830 | { __MODULE__, :hello } -> 831 | send(parent, { :hi, self() }) 832 | end 833 | close() 834 | end 835 | name = { :via, :global, :cast_via } 836 | { :ok, pid } = Core.start_link(__MODULE__, fun, via: { :global, :cast_via }) 837 | assert :ok === Core.cast(name, __MODULE__, :hello) 838 | assert_receive { :hi, ^pid }, 200, "cast was not received" 839 | close(pid) 840 | assert TestIO.binread() === <<>> 841 | end 842 | 843 | test "cast to local name that is not registered" do 844 | assert :ok === Core.cast(:cast_bad_local, __MODULE__, :hello) 845 | end 846 | 847 | test "cast to global name that is not registered" do 848 | assert :ok === Core.cast({ :global, :cast_bad_global }, __MODULE__, :hello) 849 | end 850 | 851 | test "cast to via name that is not registered" do 852 | assert :ok === Core.cast({ :via, :global, :cast_bad_via }, __MODULE__, 853 | :hello) 854 | end 855 | 856 | test "cast to local name on bad node" do 857 | assert :ok === Core.cast({ :cast_bad_local, :node_does_not_exist }, 858 | __MODULE__, :hello) 859 | end 860 | 861 | test "send to pid" do 862 | fun = fn(parent, _debug) -> 863 | Core.init_ack() 864 | receive do 865 | { __MODULE__, :hello } -> 866 | send(parent, { :hi, self() }) 867 | end 868 | close() 869 | end 870 | pid = Core.spawn_link(__MODULE__, fun) 871 | assert { __MODULE__, :hello } === Core.send(pid, { __MODULE__, :hello }) 872 | assert_receive { :hi, ^pid }, 200, "message was not received" 873 | close(pid) 874 | assert TestIO.binread() === <<>> 875 | end 876 | 877 | test "send to local name" do 878 | fun = fn(parent, _debug) -> 879 | Core.init_ack() 880 | receive do 881 | { __MODULE__, :hello } -> 882 | send(parent, { :hi, self() }) 883 | end 884 | close() 885 | end 886 | { :ok, pid } = Core.start_link(__MODULE__, fun, local: :send_local) 887 | assert { __MODULE__, :hello } === Core.send(:send_local, 888 | { __MODULE__, :hello }) 889 | assert_receive { :hi, ^pid }, 200, "message was not received" 890 | close(pid) 891 | assert TestIO.binread() === <<>> 892 | end 893 | 894 | test "send to local name with local node" do 895 | fun = fn(parent, _debug) -> 896 | Core.init_ack() 897 | receive do 898 | { __MODULE__, :hello } -> 899 | send(parent, { :hi, self() }) 900 | end 901 | close() 902 | end 903 | { :ok, pid } = Core.start_link(__MODULE__, fun, local: :send_local2) 904 | assert { __MODULE__, :hello} === Core.send({ :send_local2, node() }, 905 | { __MODULE__, :hello }) 906 | assert_receive { :hi, ^pid }, 200, "message was not received" 907 | close(pid) 908 | assert TestIO.binread() === <<>> 909 | end 910 | 911 | test "send to global name" do 912 | fun = fn(parent, _debug) -> 913 | Core.init_ack() 914 | receive do 915 | { __MODULE__, :hello } -> 916 | send(parent, { :hi, self() }) 917 | end 918 | close() 919 | end 920 | name = { :global, :send_global } 921 | { :ok, pid } = Core.start_link(__MODULE__, fun, [name]) 922 | assert { __MODULE__, :hello } === Core.send(name,{ __MODULE__, :hello }) 923 | assert_receive { :hi, ^pid }, 200, "message was not received" 924 | close(pid) 925 | assert TestIO.binread() === <<>> 926 | end 927 | 928 | test "send to via name" do 929 | fun = fn(parent, _debug) -> 930 | Core.init_ack() 931 | receive do 932 | { __MODULE__, :hello } -> 933 | send(parent, { :hi, self() }) 934 | end 935 | close() 936 | end 937 | name = { :via, :global, :send_via } 938 | { :ok, pid } = Core.start_link(__MODULE__, fun, via: { :global, :send_via }) 939 | assert { __MODULE__, :hello } === Core.send(name, { __MODULE__, :hello }) 940 | assert_receive { :hi, ^pid }, 200, "message was not received" 941 | close(pid) 942 | assert TestIO.binread() === <<>> 943 | end 944 | 945 | test "send to local name that is not registered" do 946 | assert_raise ArgumentError, 947 | "no process associated with send_bad_local", 948 | fn() -> Core.send(:send_bad_local, { __MODULE__, :hello }) end 949 | end 950 | 951 | test "send to local name with local node that is not registered" do 952 | assert_raise ArgumentError, 953 | "no process associated with send_bad_local", 954 | fn() -> Core.send({ :send_bad_local, node() }, { __MODULE__, :hello }) end 955 | end 956 | 957 | test "send to global name that is not registered" do 958 | assert_raise ArgumentError, 959 | "no process associated with :send_bad_global (global)", 960 | fn() -> 961 | Core.send({ :global, :send_bad_global }, { __MODULE__, :hello }) 962 | end 963 | end 964 | 965 | test "send to via name that is not registered" do 966 | assert_raise ArgumentError, 967 | "no process associated with :send_bad_via (global)", 968 | fn() -> 969 | Core.send({ :via, :global, :send_bad_via }, { __MODULE__, :hello }) 970 | end 971 | end 972 | 973 | ## utils 974 | 975 | defp linked?(pid) do 976 | { :links, links } = Process.info(self(), :links) 977 | Enum.member?(links, pid) 978 | end 979 | 980 | defp close(pid) do 981 | ref = Process.monitor(pid) 982 | Process.unlink(pid) 983 | send(pid, :close) 984 | receive do 985 | { :DOWN, ^ref, _, _, reason } -> 986 | reason 987 | after 988 | 500 -> 989 | Process.demonitor(ref, [:flush]) 990 | Process.exit(pid, :kill) 991 | :timeout 992 | end 993 | end 994 | 995 | defp close() do 996 | receive do 997 | :close -> 998 | exit(:normal) 999 | after 1000 | 500 -> 1001 | exit(:timeout) 1002 | end 1003 | end 1004 | 1005 | defp erl_inspect(term) do 1006 | List.to_string(:io_lib.format('~p', [term])) 1007 | end 1008 | 1009 | end 1010 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | defmodule TestIO do 4 | 5 | use ExUnit.Callbacks 6 | use GenEvent 7 | 8 | def setup_all() do 9 | :ok = :error_logger.add_report_handler(TestIO) 10 | :error_logger.tty(false) 11 | on_exit(fn() -> 12 | :error_logger.tty(true) 13 | :error_logger.delete_report_handler(TestIO) 14 | end) 15 | :ok 16 | end 17 | 18 | def setup() do 19 | stdio = Process.group_leader() 20 | { :ok, stringio } = StringIO.open(<<>>) 21 | Process.group_leader(self(), stringio) 22 | on_exit(fn() -> 23 | Process.group_leader(self(), stdio) 24 | end) 25 | :ok 26 | end 27 | 28 | def binread() do 29 | # sync with :error_logger so that everything sent by current process has 30 | # been written. Also checks handler is alive and writing to StringIO. 31 | :pong = :gen_event.call(:error_logger, TestIO, :ping, 5000) 32 | { input, output } = StringIO.contents(Process.group_leader()) 33 | << input :: binary, output :: binary >> 34 | end 35 | 36 | def init(_args) do 37 | { :ok, nil } 38 | end 39 | 40 | def handle_event({ :error, device, { _pid, format, data } }, state) do 41 | try do 42 | :io.format(device, format ++ '~n', data) 43 | catch 44 | # device can receive exit signal from parent causing it to exit 45 | # before replying. 46 | :error, :terminated -> 47 | :ok 48 | end 49 | { :ok, state } 50 | end 51 | 52 | def handle_event(_other, state) do 53 | { :ok, state } 54 | end 55 | 56 | def handle_call(:ping, state) do 57 | { :ok, :pong, state } 58 | end 59 | 60 | def terminate({ :error, reason }, _state) do 61 | IO.puts(:user, "error in TestIO: #{inspect(reason)}") 62 | end 63 | 64 | def terminate(_reason, _state) do 65 | :ok 66 | end 67 | 68 | end 69 | 70 | defmodule GS do 71 | 72 | use GenServer 73 | 74 | def start_link(fun, debug_opts \\ []) do 75 | :gen_server.start_link(__MODULE__, fun, [{ :debug, debug_opts }]) 76 | end 77 | 78 | def init(fun), do: fun.() 79 | 80 | def code_change(_oldvsn, _oldfun, newfun), do: newfun.() 81 | 82 | end 83 | 84 | defmodule GE do 85 | 86 | use GenEvent 87 | 88 | def start_link(fun) do 89 | { :ok, pid } = :gen_event.start_link() 90 | :ok = :gen_event.add_handler(pid, __MODULE__, fun) 91 | { :ok, pid } 92 | end 93 | 94 | def init(fun), do: fun.() 95 | 96 | def code_change(_oldvsn, _oldfun, newfun), do: newfun.() 97 | 98 | end 99 | 100 | defmodule GFSM do 101 | 102 | @behaviour :gen_fsm 103 | 104 | def start_link(fun, debug_opts \\ []) do 105 | :gen_fsm.start_link(__MODULE__, fun, [{ :debug, debug_opts }]) 106 | end 107 | 108 | def init(fun), do: fun.() 109 | 110 | def state(_event, fun) do 111 | { :next_state, :state, fun } 112 | end 113 | 114 | def state(_event, _from, fun) do 115 | { :next_state, :state, fun } 116 | end 117 | 118 | def handle_event(_evemt, :state, fun) do 119 | { :next_state, :state, fun } 120 | end 121 | 122 | def handle_sync_event(_event, _from, :state, fun) do 123 | { :next_state, :state, fun } 124 | end 125 | 126 | def handle_info(_info, :state, fun) do 127 | { :next_state, :state, fun } 128 | end 129 | 130 | def code_change(_oldvsn, :state, _oldfun, extra) do 131 | extra.() 132 | end 133 | 134 | def terminate(_reason, :state, _fun) do 135 | :ok 136 | end 137 | 138 | end 139 | --------------------------------------------------------------------------------