diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..aa10f0b --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,153 @@ +version: 2.1 # use CircleCI 2.1 instead of CircleCI Classic + +x-common: + job-common: &job-common + working_directory: /home/circleci/project + docker: + - image: circleci/elixir:1.11 + environment: + MIX_ARCHIVES: /home/circleci/project/.mix/archives + MIX_HOME: /home/circleci/project/.mix + deploy-common: &deploy-common + working_directory: /home/circleci/project + docker: + - image: circleci/buildpack-deps + +x-steps: + - &save-deps-cache + save_cache: + key: v1-deps-cache-{{ .Branch }}-{{ checksum "mix.lock" }} + paths: ["deps"] + - &restore-deps-cache + restore_cache: + keys: + - v1-deps-cache-{{ .Branch }}-{{ checksum "mix.lock" }} + - &save-plt-cache + save_cache: + key: v1-plt-cache-{{ .Branch }}-{{ checksum ".dialyzer/cache.plt" }} + paths: [".dialyzer/cache.plt"] + - &restore-plt-cache + restore_cache: + keys: + - v1-plt-cache-{{ .Branch }}-{{ checksum ".dialyzer/cache.plt" }} + - &attach-workspace + attach_workspace: + at: /home/circleci + +x-filters: + only-pr: &only-pr + branches: + ignore: /^(master)$/ + only-master: &only-master + branches: + only: /^master$/ + +jobs: + setup: + <<: *job-common + steps: + - checkout + - *restore-deps-cache + - run: + name: Install Hex + command: mix local.hex --force --if-missing + - run: + name: Install rebar + command: mix local.rebar --force --if-missing + - run: + name: Install project's deps + command: mix deps.get + - *save-deps-cache + - persist_to_workspace: + root: /home/circleci/ + paths: ["project"] + + build: + <<: *job-common + steps: + - *attach-workspace + - run: + name: Build project + command: mix compile --warnings-as-errors + - persist_to_workspace: + root: /home/circleci/ + paths: ["project/_build"] + + format: + <<: *job-common + steps: + - *attach-workspace + - run: + name: Check formatting rules + command: mix format --check-formatted + + dialyzer: + <<: *job-common + steps: + - *attach-workspace + - run: + name: Dialyzer static analysis + command: mix dialyzer + + credo: + <<: *job-common + steps: + - *attach-workspace + - run: + name: Check coding rules + command: mix credo + + test: + <<: *job-common + steps: + - *attach-workspace + - run: mix test + +workflows: + version: 2 + on-pull-request: + jobs: + - setup: + filters: + <<: *only-pr + - build: + requires: + - setup + filters: + <<: *only-pr + - format: + requires: + - setup + filters: + <<: *only-pr + - credo: + requires: + - build + filters: + <<: *only-pr + - dialyzer: + requires: + - build + filters: + <<: *only-pr + - test: + requires: + - build + filters: + <<: *only-pr + + on-merge: + jobs: + - setup: + filters: + <<: *only-master + - build: + requires: + - setup + filters: + <<: *only-master + - test: + requires: + - setup + filters: + <<: *only-master diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..3607e1a --- /dev/null +++ b/.credo.exs @@ -0,0 +1,189 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + {Credo.Check.Design.TagFIXME, []}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, false}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + # {Credo.Check.Refactor.MapInto, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + # {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.MixEnv, false}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.UnsafeExec, []}, + + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just replace `false` with `[]`) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + {Credo.Check.Consistency.UnusedVariableNames, false}, + {Credo.Check.Design.DuplicatedCode, false}, + {Credo.Check.Readability.AliasAs, false}, + {Credo.Check.Readability.BlockPipe, false}, + {Credo.Check.Readability.ImplTrue, false}, + {Credo.Check.Readability.MultiAlias, false}, + {Credo.Check.Readability.SeparateAliasRequire, false}, + {Credo.Check.Readability.SinglePipe, false}, + {Credo.Check.Readability.Specs, false}, + {Credo.Check.Readability.StrictModuleLayout, false}, + {Credo.Check.Readability.WithCustomTaggedTuple, false}, + {Credo.Check.Refactor.ABCSize, false}, + {Credo.Check.Refactor.AppendSingleItem, false}, + {Credo.Check.Refactor.DoubleBooleanNegation, false}, + {Credo.Check.Refactor.ModuleDependencies, false}, + {Credo.Check.Refactor.NegatedIsNil, false}, + {Credo.Check.Refactor.PipeChainStart, false}, + {Credo.Check.Refactor.VariableRebinding, false}, + {Credo.Check.Warning.LeakyEnvironment, false}, + {Credo.Check.Warning.MapGetUnsafePass, false}, + {Credo.Check.Warning.UnsafeToAtom, false} + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + ] +} diff --git a/.dialyzer/ignore.exs b/.dialyzer/ignore.exs new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/.dialyzer/ignore.exs @@ -0,0 +1 @@ +[] diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml deleted file mode 100644 index 372ff7d..0000000 --- a/.github/workflows/elixir.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Elixir CI - -on: push - -jobs: - build: - - runs-on: ubuntu-latest - - container: - image: elixir:1.9.1-slim - - steps: - - uses: actions/checkout@v1 - - name: Install Dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get - - name: Run Tests - run: mix test diff --git a/.gitignore b/.gitignore index 631e82f..90920f2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). coap-*.tar +*.plt +*.plt.hash diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b7f465f --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2018-2020 Tony Pitale +Copyright (c) 2019-2020 John Giffin +Copyright (c) 2020 Jean Parpaillon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index a09b19d..0c9671f 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,49 @@ Simple client usage: CoAP.Client.get("coap://localhost:5683/api/healthcheck") ``` +# Telemetry # + +Coap_ex emits telemetry events for data sent and received, block transfers, and other connection-releated events. To consume them, attach a handler in your app startup like so: + +``` +:telemetry.attach_many( + "myapp-coap-ex-connection", + [ + [:coap_ex, :connection, :block_sent], + [:coap_ex, :connection, :block_received], + [:coap_ex, :connection, :connection_started], + [:coap_ex, :connection, :connection_ended], + [:coap_ex, :connection, :data_sent], + [:coap_ex, :connection, :data_received], + [:coap_ex, :connection, :re_tried], + [:coap_ex, :connection, :timed_out] + ], + &MyHandler.handle_event/4, + nil +) +``` + +Each connection can be tagged when the connection is created, and this tag will be passed to the telemetry handler. This makes it possible to monitor a single connection among many connections. The tag can be any value. + +To tag a client connection, pass a tag in the request options: + +``` +CoAP.Client.request( + :con, + method, + url, + request_payload, + %{retries: retries, timeout: @wait_timeout, ack_timeout: timeout, tag: tag} +) + ``` + + To tag a server connection if using Phoenix: + + ``` + # in phoenix controller + CoAP.Phoenix.Conn.tag(conn, tag) + ``` + # TODO: * [x] handle multiple parts for some headers, like "Uri-Path" diff --git a/lib/coap.ex b/lib/coap.ex index 1fc18e7..78f3d8c 100644 --- a/lib/coap.ex +++ b/lib/coap.ex @@ -2,8 +2,6 @@ defmodule CoAP do use Application def start(_type, _args) do - import Supervisor.Spec, warn: false - children = [ {DynamicSupervisor, name: CoAP.SocketServerSupervisor, strategy: :one_for_one}, {DynamicSupervisor, name: CoAP.HandlerSupervisor, strategy: :one_for_one}, diff --git a/lib/coap/adapters/phoenix.ex b/lib/coap/adapters/phoenix.ex index 332b2a6..48bb0f7 100644 --- a/lib/coap/adapters/phoenix.ex +++ b/lib/coap/adapters/phoenix.ex @@ -29,7 +29,6 @@ defmodule CoAP.Adapters.Phoenix do end defp process(req, {endpoint, opts}) do - # TODO: conn_from_message(message) case endpoint.__handler__(@connection.conn(req), opts) do {:plug, conn, handler, opts} -> %{adapter: {@connection, _req}} = diff --git a/lib/coap/block.ex b/lib/coap/block.ex index 9931496..eb915a2 100644 --- a/lib/coap/block.ex +++ b/lib/coap/block.ex @@ -8,7 +8,7 @@ defmodule CoAP.Block do @type binary_t_large :: <<_::32>> @type binary_t :: binary_t_small | binary_t_medium | binary_t_large - # TODO: if more: false, a size_exponent of 0 should be ignored? + # _TODO: if more: false, a size_exponent of 0 should be ignored? # otherwise size_exponent of 0 results in size: 16 @doc """ @@ -67,32 +67,27 @@ defmodule CoAP.Block do } end - @spec encode(t()) :: binary_t() + @spec encode(t() | tuple_t()) :: binary_t() def encode(%__MODULE__{} = block), do: encode({block.number, block.more, block.size}) @spec encode(%{number: integer, more: 0 | 1, size: integer}) :: binary_t() def encode(%{number: number, more: more, size: size}), do: encode({number, more, size}) - @spec encode({integer, boolean, 0}) :: binary_t() def encode({number, more, 0}) do encode(number, if(more, do: 1, else: 0), 0) end - @spec encode({integer, boolean, integer}) :: binary_t() def encode({number, more, size}) do encode(number, if(more, do: 1, else: 0), trunc(:math.log2(size)) - 4) end - @spec encode(integer, 0 | 1, integer) :: binary_t_small() def encode(number, more, size_exponent) when number < 16 do <> end - @spec encode(integer, 0 | 1, integer) :: binary_t_medium() def encode(number, more, size_exponent) when number < 4096 do <> end - @spec encode(integer, 0 | 1, integer) :: binary_t_large() def encode(number, more, size_exponent) do <> end diff --git a/lib/coap/client.ex b/lib/coap/client.ex index bd5e0ea..2aeb3ee 100644 --- a/lib/coap/client.ex +++ b/lib/coap/client.ex @@ -32,7 +32,7 @@ defmodule CoAP.Client do tag: nil end - # TODO: options: headers/params? + # _TODO: options: headers/params? @doc """ Perform a confirmable, GET request to a URL diff --git a/lib/coap/connection.ex b/lib/coap/connection.ex index 771214a..27d2890 100644 --- a/lib/coap/connection.ex +++ b/lib/coap/connection.ex @@ -150,7 +150,7 @@ defmodule CoAP.Connection do @default_payload_size 512 # 16 bit number - @max_message_id 65535 + @max_message_id 65_535 def child_spec([server, endpoint, peer, config]) do %{ @@ -163,8 +163,6 @@ defmodule CoAP.Connection do def start_link(args), do: GenServer.start_link(__MODULE__, args) - # TODO: predefined defaults, merged with client/server-specific options - # TODO: default adapter to GenericServer? @doc """ `init` functions for Server and Client processes @@ -179,6 +177,7 @@ defmodule CoAP.Connection do Wrap the adapter and the client in a handler """ + @impl GenServer def init([server, {adapter, endpoint}, {ip, port, token} = _peer, config]) do {:ok, handler} = start_handler(adapter, endpoint) @@ -213,11 +212,8 @@ defmodule CoAP.Connection do |> State.add_options(options)} end + @impl GenServer def handle_info({:receive, %Message{} = message}, state) do - # TODO: connection timeouts - # TODO: start timer for conn - # TODO: check if the message_id matches what we may have already sent? - :telemetry.execute( [:coap_ex, :connection, :data_received], %{size: message.raw_size}, @@ -251,7 +247,7 @@ defmodule CoAP.Connection do def handle_info({:tag, tag}, state), do: {:noreply, %{state | tag: tag}} - # TODO: connection timeout, set to original state? + # _TODO: connection timeout, set to original state? # def handle_info(:retry, state) @@ -260,17 +256,17 @@ defmodule CoAP.Connection do # RECEIVE ==================================================================== # con -> reset - # TODO: how do we get a nil method, vs a response + # _TODO: how do we get a nil method, vs a response # defp receive_message(%Message{method: nil, type: :con} = message, %{phase: :idle} = state) do - # TODO: peer ack with reset, next state is peer_ack_sent + # _TODO: peer ack with reset, next state is peer_ack_sent # Message.response_for(message) # reply(:reset, message, state[:server]) # end - # TODO: resend reset? - # TODO: what is the message if the client has to re-request after a processing timeout from the app? + # _TODO: resend reset? + # _TODO: what is the message if the client has to re-request after a processing timeout from the app? - # TODO: resend stored message (ack) + # _TODO: resend stored message (ack) defp receive_message(_message, %{phase: :peer_ack_sent} = state), do: state # Do nothing if we receive a message from peer during these states; we should be shutting down @@ -355,7 +351,7 @@ defmodule CoAP.Connection do ) do timer = restart_timer(state.timer, ack_timeout(state)) - # TODO: respect the number/size from control + # _TODO: respect the number/size from control # more must be false, must use same size on subsequent request multipart = Multipart.build(nil, Block.build({number + 1, false, size})) @@ -363,7 +359,7 @@ defmodule CoAP.Connection do response = case message.status do # client sends original message with new control number - # TODO: what parts of the message are we supposed to send back? + # _TODO: what parts of the message are we supposed to send back? {:ok, _} -> Message.next_message(state.message, next_message_id(state.message.message_id)) @@ -456,7 +452,7 @@ defmodule CoAP.Connection do defp receive_message(message, %{phase: :awaiting_peer_ack} = state) do cancel_timer(state.timer) - # TODO: what happens if this is an empty ACK and we get another response later? + # _TODO: what happens if this is an empty ACK and we get another response later? # Could we go back to being idle? handle(message, state.handler, peer_for(state)) @@ -464,17 +460,17 @@ defmodule CoAP.Connection do end # DELIVER ==================================================================== - # TODO: deliver message for got_non as a NON message, phase becomes :sent_non - # TODO: deliver message for peer_ack_sent as a CON message, phase becomes :awaiting_peer_ack + # _TODO: deliver message for got_non as a NON message, phase becomes :sent_non + # _TODO: deliver message for peer_ack_sent as a CON message, phase becomes :awaiting_peer_ack defp deliver_message(message, %{phase: :awaiting_app_ack} = state) do - # TODO: does the message include the original request control? + # _TODO: does the message include the original request control? {bytes, block, payload} = Payload.segment_at(message.payload, @default_payload_size, 0) # Cancel the app_ack waiting timeout cancel_timer(state.timer) - # TODO: what happens if the app response is a status code, no code_class/detail tuple? + # _TODO: what happens if the app response is a status code, no code_class/detail tuple? response = Message.response_for( {message.code_class, message.code_detail}, @@ -501,7 +497,7 @@ defmodule CoAP.Connection do %Message{type: type, message_id: message_id} = message, %{phase: :idle, next_message_id: next_message_id} = state ) do - # TODO: get payload size from the request control + # _TODO: get payload size from the request control {bytes, block, payload} = Payload.segment_at(message.payload, @default_payload_size, 0) # The server should send back the same message id of the request @@ -562,7 +558,6 @@ defmodule CoAP.Connection do ack_timeout(state) _ -> - # TODO: exponential backoff timeout * 2 end @@ -619,7 +614,7 @@ defmodule CoAP.Connection do # REQUEST ==================================================================== defp handle(message, handler, peer) do - send(handler, {direction(message), message, peer, self()}) + send(handler, {direction(message), message, peer}) end # RESPOND ==================================================================== @@ -676,7 +671,6 @@ defmodule CoAP.Connection do end # HANDLER - # TODO: move to CoAP defp start_handler({adapter, endpoint}), do: start_handler(adapter, endpoint) defp start_handler(adapter, endpoint) do @@ -684,12 +678,11 @@ defmodule CoAP.Connection do CoAP.HandlerSupervisor, { CoAP.Handler, - [adapter, endpoint] + [adapter, endpoint, self()] } ) end - # TODO: move to CoAP defp start_socket_for(endpoint, peer) do DynamicSupervisor.start_child( CoAP.SocketServerSupervisor, diff --git a/lib/coap/handler.ex b/lib/coap/handler.ex index 710dbe8..d3cdb42 100644 --- a/lib/coap/handler.ex +++ b/lib/coap/handler.ex @@ -1,41 +1,47 @@ defmodule CoAP.Handler do - use GenServer - @moduledoc """ Thin wrapper process started for each connection with a given `endpoint` module Handles receiving messages from connection via request/response and calling the appropriate endpoint functions Calls back to the connection with the results of the function call, using :deliver """ + use GenServer + + defstruct adapter: nil, endpoint: nil, connection: nil, ref: nil + def child_spec(args) do + %{id: __MODULE__, start: {__MODULE__, :start_link, [args]}, restart: :transient} + end + + @doc false def start_link(args) do GenServer.start_link(__MODULE__, args) end - def init([adapter, endpoint]) do - {:ok, {adapter, endpoint}} + @impl GenServer + def init([adapter, endpoint, connection]) do + ref = Process.monitor(connection) + s = %__MODULE__{adapter: adapter, endpoint: endpoint, connection: connection, ref: ref} + {:ok, s} end - # TODO: this process is blocked when calling endpoint - # TODO: we may want to instrument/log the queue depth here - # It _should not_ be an issue because this process is already per-peer connection - - def handle_info({:request, message, peer, connection}, {adapter, endpoint} = state) do - adapter.request(message, {endpoint, peer}, connection) - {:noreply, state} + @impl GenServer + def handle_info({:request, message, peer}, s) do + s.adapter.request(message, {s.endpoint, peer}, s.connection) + {:noreply, s} end - def handle_info({:response, message, peer, connection}, {adapter, endpoint} = state) do - adapter.response(message, {endpoint, peer}, connection) - {:noreply, state} + def handle_info({:response, message, peer}, s) do + s.adapter.response(message, {s.endpoint, peer}, s.connection) + {:noreply, s} end - def handle_info({:error, reason}, {adapter, endpoint} = state) do - adapter.error({endpoint, %{reason: reason}}) - {:noreply, state} + def handle_info({:error, reason}, s) do + s.adapter.error({s.endpoint, %{reason: reason}}) + {:noreply, s} end - # defp deliver(result, connection) do - # send(connection, {:deliver, result}) - # end + def handle_info({:DOWN, ref, :process, _pid, _reason}, %__MODULE__{ref: ref} = s) do + {:stop, :normal, s} + end end diff --git a/lib/coap/message.ex b/lib/coap/message.ex index 2b840a6..e32be19 100644 --- a/lib/coap/message.ex +++ b/lib/coap/message.ex @@ -64,18 +64,20 @@ defmodule CoAP.Message do @type status_code :: {integer, integer} @type status_t :: nil | {atom, atom} + @type request_type :: :con | :non | :ack | :reset + @type t :: %__MODULE__{ version: integer, type: request_type, - request: boolean, + request: boolean | nil, code_class: integer, code_detail: integer, - method: request_method | {integer, integer}, + method: request_method | nil | {integer, integer}, status: status_t, message_id: integer, token: binary, options: map, - multipart: CoAP.Multipart.t(), + multipart: CoAP.Multipart.t() | nil, payload: binary, raw_size: integer } @@ -88,8 +90,6 @@ defmodule CoAP.Message do } @types_map Enum.into(@types, %{}, fn {k, v} -> {v, k} end) - @type request_type :: :con | :non | :ack | :reset - @message_header_format (quote do << var!(version)::unsigned-integer-size(2), @@ -130,11 +130,10 @@ defmodule CoAP.Message do # Always check code_detail in case the message was made directly, not decoded blocks = Multipart.as_blocks(request?(message.code_class), message.multipart) - %{message | options: Map.merge(message.options, blocks), multipart: nil} + %__MODULE__{message | options: Map.merge(message.options, blocks), multipart: nil} |> encode() end - @spec encode(t()) :: binary def encode(%__MODULE__{ version: version, type: type, @@ -317,26 +316,26 @@ defmodule CoAP.Message do %CoAP.Multipart{multipart: false} """ - # TODO: test if either block1 or block2 is nil + # _TODO: test if either block1 or block2 is nil @spec multipart(boolean, %{block1: CoAP.Block.tuple_t(), block2: CoAP.Block.tuple_t()}) :: CoAP.Multipart.t() def multipart(request, options) do Multipart.build(request, options[:block1], options[:block2]) end - @spec request?(0) :: true + @spec request?(any) :: boolean defp request?(0), do: true - @spec request?(any) :: false + defp request?(_), do: false - @spec method_for(0, integer) :: request_method() + @spec method_for(any, any) :: request_method() | nil defp method_for(0, code_detail), do: @methods[{0, code_detail}] - @spec method_for(any, any) :: nil + defp method_for(_code_class, _code_detail), do: nil - @spec status_for(0, any) :: nil + @spec status_for(integer, any) :: nil | status_t defp status_for(0, _code_detail), do: nil - @spec status_for(integer, integer) :: status_t + defp status_for(code_class, code_detail), do: @methods[{code_class, code_detail}] @doc """ @@ -371,7 +370,7 @@ defmodule CoAP.Message do """ @spec next_message(t(), integer) :: t() def next_message(%__MODULE__{} = message, next_message_id) do - %{message | message_id: next_message_id, payload: <<>>, multipart: nil} + %__MODULE__{message | message_id: next_message_id, payload: <<>>, multipart: nil} end @doc """ @@ -386,7 +385,6 @@ defmodule CoAP.Message do } end - @spec response_for(t()) :: t() def response_for(%__MODULE__{type: :non} = message) do %__MODULE__{ type: :non, diff --git a/lib/coap/message_options.ex b/lib/coap/message_options.ex index de1416a..7bace3a 100644 --- a/lib/coap/message_options.ex +++ b/lib/coap/message_options.ex @@ -1,8 +1,6 @@ defmodule CoAP.MessageOptions do # @payload_marker 0xFF - # TODO: struct for all options? - @doc """ Examples @@ -69,7 +67,6 @@ defmodule CoAP.MessageOptions do {tail, delta_sum + delta} delta == 13 -> - # TODO: size here `::size(4)`? <> = tail {new_tail1, delta_sum + key + 13} @@ -84,7 +81,6 @@ defmodule CoAP.MessageOptions do {tail1, length} length == 13 -> - # TODO: size here `::size(4)`? <> = tail1 {new_tail2, extended_option_length + 13} @@ -175,7 +171,6 @@ defmodule CoAP.MessageOptions do acc::binary, delta::size(4), length::size(4), - # TODO: what size should this be? extended_number::binary, extended_length::binary, value::binary diff --git a/lib/coap/multipart.ex b/lib/coap/multipart.ex index a8bce17..fe40662 100644 --- a/lib/coap/multipart.ex +++ b/lib/coap/multipart.ex @@ -11,7 +11,6 @@ defmodule CoAP.Multipart do alias CoAP.Block - # TODO: redefine as description/control based on request/response defstruct multipart: false, description: nil, control: nil, @@ -32,22 +31,19 @@ defmodule CoAP.Multipart do requested_number: integer } - @spec build(any, nil, nil) :: t() + @spec build(any, any, any) :: t() def build(_request, nil, nil), do: %__MODULE__{} # Request variation - @spec build(boolean, CoAP.Block.t(), CoAP.Block.t()) :: t() def build(true, block1, block2) do build(Block.build(block1), Block.build(block2)) end # Response variation - @spec build(boolean, CoAP.Block.t(), CoAP.Block.t()) :: t() def build(false, block1, block2) do build(Block.build(block2), Block.build(block1)) end - @spec build(CoAP.Block.t(), CoAP.Block.t()) :: t() def build(%Block{} = description, %Block{} = control) do %__MODULE__{ multipart: true, @@ -61,7 +57,7 @@ defmodule CoAP.Multipart do } end - @spec build(nil, CoAP.Block.t()) :: t() + @spec build(CoAP.Block.t() | nil, CoAP.Block.t() | nil) :: t() def build(nil, %Block{} = control) do %__MODULE__{ multipart: true, @@ -72,7 +68,6 @@ defmodule CoAP.Multipart do } end - @spec build(CoAP.Block.t(), nil) :: t() def build(%Block{} = description, nil) do case {description.more, description.number} do {false, 0} -> @@ -92,10 +87,12 @@ defmodule CoAP.Multipart do end end - @spec build(nil, nil) :: t() def build(nil, nil), do: %__MODULE__{multipart: false, description: nil, control: nil} - @spec as_blocks(true, CoAP.Multipart.t()) :: %{block1: CoAP.Block.t(), block2: CoAP.Block.t()} + @spec as_blocks(boolean, CoAP.Multipart.t()) :: %{ + block1: CoAP.Block.t(), + block2: CoAP.Block.t() + } def as_blocks(true, multipart) do %{ block1: multipart.description |> Block.to_tuple(), @@ -104,8 +101,6 @@ defmodule CoAP.Multipart do |> reject_nil_values() end - # TODO: if we get nil here, that's wrong - @spec as_blocks(false, CoAP.Multipart.t()) :: %{block1: CoAP.Block.t(), block2: CoAP.Block.t()} def as_blocks(false, multipart) do %{ block1: multipart.control |> Block.to_tuple(), diff --git a/lib/coap/payload.ex b/lib/coap/payload.ex index 1ecc435..abad595 100644 --- a/lib/coap/payload.ex +++ b/lib/coap/payload.ex @@ -74,7 +74,6 @@ defmodule CoAP.Payload do part_size = Enum.min([data_size - offset, size]) more = data_size > offset + part_size - # TODO: splits into the appropriate segment data = data |> :binary.part(offset, part_size) block = Block.build({number, more, size}) diff --git a/lib/coap/phoenix/conn.ex b/lib/coap/phoenix/conn.ex index 1b303a0..d73f10e 100644 --- a/lib/coap/phoenix/conn.ex +++ b/lib/coap/phoenix/conn.ex @@ -27,14 +27,11 @@ defmodule CoAP.Phoenix.Conn do query_string: qs, req_headers: to_headers_list(headers), request_path: path, - # TODO: coaps scheme: "coap" } end def send_resp(req, status, _headers, body) do - # IO.puts("#{inspect(req)} returns #{inspect(status)}, #{inspect(headers)}, #{inspect(body)}") - message = req.message connection = req.owner @@ -46,7 +43,6 @@ defmodule CoAP.Phoenix.Conn do code_detail: code_detail, message_id: message.message_id, token: message.token, - # TODO: options from filtered headers payload: body } diff --git a/lib/coap/phoenix/listener.ex b/lib/coap/phoenix/listener.ex index 3a21407..ace5a5c 100644 --- a/lib/coap/phoenix/listener.ex +++ b/lib/coap/phoenix/listener.ex @@ -30,16 +30,15 @@ defmodule CoAP.Phoenix.Listener do GenServer.start_link(__MODULE__, endpoint) end - # TODO: spec for this def init(endpoint) do - # TODO: take this config and use it to start a CoAP.SocketServer + # _TODO: take this config and use it to start a CoAP.SocketServer config = endpoint.config(:coap) info("Starting CoAP.Phoenix.Listener: #{inspect(config)}") {:ok, server} = CoAP.SocketServer.start_link([{@adapter, endpoint}, config[:port], config]) - # TODO: ref and monitor? - # TODO: die if server dies? + # _TODO: ref and monitor? + # _TODO: die if server dies? {:ok, %{endpoint: endpoint, config: config, server: server}} end diff --git a/lib/coap/phoenix/request.ex b/lib/coap/phoenix/request.ex index 914d9e7..a8afd65 100644 --- a/lib/coap/phoenix/request.ex +++ b/lib/coap/phoenix/request.ex @@ -66,7 +66,6 @@ defmodule CoAP.Phoenix.Request do def build(%Message{options: options} = message, {address, port}, owner, config \\ %{}) do _ip_string = Enum.join(Tuple.to_list(address), ".") - # TODO: defstruct? %{ method: message |> method, path: options[:uri_path] |> Enum.join("/"), diff --git a/lib/coap/records.ex b/lib/coap/records.ex deleted file mode 100644 index 3afe339..0000000 --- a/lib/coap/records.ex +++ /dev/null @@ -1,21 +0,0 @@ -defmodule CoAP.Records do - require Record - - # TODO: remove/deprecate - - # Record.defrecord( - # :coap_content, - # Record.extract( - # :coap_content, - # from_lib: "gen_coap/include/coap.hrl" - # ) - # ) - # - # Record.defrecord( - # :coap_message, - # Record.extract( - # :coap_message, - # from_lib: "gen_coap/include/coap.hrl" - # ) - # ) -end diff --git a/lib/coap/socket_server.ex b/lib/coap/socket_server.ex index 7a3bd27..ff12c8b 100644 --- a/lib/coap/socket_server.ex +++ b/lib/coap/socket_server.ex @@ -57,7 +57,6 @@ defmodule CoAP.SocketServer do def init([endpoint, {host, port, token}, connection]) do {:ok, socket} = :gen_udp.open(0, [:binary, {:active, true}, {:reuseaddr, true}]) - # TODO: use handle_continue to do this ip = normalize_host(host) connection_id = {ip, port, token} @@ -154,8 +153,6 @@ defmodule CoAP.SocketServer do }:#{inspect(connection)}:#{inspect(ref)}" ) - # TODO: handle noproc - {:noreply, %{ state @@ -213,7 +210,6 @@ defmodule CoAP.SocketServer do end end - # TODO: move to CoAP defp start_connection(server, endpoint, peer, config) do DynamicSupervisor.start_child( CoAP.ConnectionSupervisor, diff --git a/mix.exs b/mix.exs index 45e06f4..fa72648 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,8 @@ defmodule Coap.MixProject do version: "0.1.0", elixir: "~> 1.6", start_permanent: Mix.env() == :prod, - deps: deps() + deps: deps(), + dialyzer: dialyzer() ] end @@ -22,12 +23,24 @@ defmodule Coap.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:plug, "~> 1.0"}, - {:gen_coap, github: "gotthardp/gen_coap", only: [:dev, :test]}, + # Dev + {:dialyxir, "~> 1.0.0", only: :dev, runtime: false}, + {:credo, "~> 1.5", only: :dev}, {:stream_data, "~> 0.1", only: :test}, - {:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false}, - {:ex_doc, "~> 0.19", only: :dev, runtime: false}, + {:ex_doc, "~> 0.23", only: :dev, runtime: false}, + + # Runtime + {:plug, "~> 1.11"}, {:telemetry, "~> 0.4.0"} ] end + + defp dialyzer do + [ + plt_ignore_apps: [:credo], + plt_add_apps: [:ex_unit, :mix], + ignore_warnings: ".dialyzer/ignore.exs", + plt_file: {:no_warn, ".dialyzer/cache.plt"} + ] + end end diff --git a/mix.lock b/mix.lock index 93b8a8d..cfb1f89 100644 --- a/mix.lock +++ b/mix.lock @@ -1,14 +1,20 @@ %{ - "dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "49496d63267bc1a4614ffd5f67c45d9fc3ea62701a6797975bc98bc156d2763f"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "credo": {:hex, :credo, "1.5.2", "5562f1a1693f77e7319fdabac6d17d26de7e6b0a2b57743bca24a89469232f04", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7003506f069866a4e5d6216a7216823b00ed4bcc4bd9c6e449fa6625c411649b"}, + "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm", "e3be2bc3ae67781db529b80aa7e7c49904a988596e2dbff897425b48b3581161"}, - "erlex": {:hex, :erlex, "0.2.1", "cee02918660807cbba9a7229cae9b42d1c6143b768c781fa6cee1eaf03ad860b", [:mix], [], "hexpm", "df65aa8e1e926941982b208f5957158a52b21fbba06ba8141fff2b8c5ce87574"}, - "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "8e24fc8ff9a50b9f557ff020d6c91a03cded7e59ac3e0eec8a27e771430c7d27"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "gen_coap": {:git, "https://github.com/gotthardp/gen_coap.git", "245e2205cf233e94e2d481f6ddda5da038ebc6ea", []}, - "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5fbc8e549aa9afeea2847c0769e3970537ed302f93a23ac612602e805d9d1e7f"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "adf0218695e22caeda2820eaba703fa46c91820d53813a2223413da3ef4ba515"}, - "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm", "5e839994289d60326aa86020c4fbd9c6938af188ecddab2579f07b66cd665328"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"}, - "plug": {:hex, :plug, "1.6.2", "e06a7bd2bb6de5145da0dd950070110dce88045351224bd98e84edfdaaf5ffee", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "b2713c085797012661a6c10e1898a009f0cc2bae556cc00341b69600074c757b"}, - "stream_data": {:hex, :stream_data, "0.4.2", "fa86b78c88ec4eaa482c0891350fcc23f19a79059a687760ddcf8680aac2799b", [:mix], [], "hexpm", "54d6bf6f1e5e27fbf4a7784a2bffbb993446d0efd079debca0f27bf859c0d1cf"}, - "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm", "e9e3cacfd37c1531c0ca70ca7c0c30ce2dbb02998a4f7719de180fe63f8d41e4"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, + "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, + "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"}, + "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, + "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, } diff --git a/test/coap/multipart_test.exs b/test/coap/multipart_test.exs index 60d0599..16d280f 100644 --- a/test/coap/multipart_test.exs +++ b/test/coap/multipart_test.exs @@ -1,7 +1,4 @@ defmodule CoAP.MultipartTest do use ExUnit.Case doctest CoAP.Multipart - - # TODO: test build - # TODO: test as_blocks end diff --git a/test/coap/payload_test.exs b/test/coap/payload_test.exs index 8364c07..1f8a675 100644 --- a/test/coap/payload_test.exs +++ b/test/coap/payload_test.exs @@ -4,8 +4,6 @@ defmodule CoAP.PayloadTest do alias CoAP.Payload - # TODO: test next_segment with StreamData - describe "to_binary/1" do test "keeps a unique set of blocks by number" do segment = <<0, 0, 0, 0, 1>>