diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..9611b10 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,31 @@ +name: Integration tests + +on: + pull_request: + branches: + - 'master' + push: + branches: + - 'master' + +jobs: + build: + name: OTP ${{ matrix.otp_version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + container: + image: erlang:${{matrix.otp_version}} + + strategy: + matrix: + otp_version: ['27', '25', '23'] + os: [ubuntu-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Compile + run: rebar3 compile + - name: Dialyzer + run: rebar3 as test dialyzer + - name: EUnit + run: TERM=xterm rebar3 eunit diff --git a/.gitignore b/.gitignore index 929000d..d306af0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,12 @@ doc/edoc-info doc/erlang.png ebin/* .* +!.github _build erl_crash.dump *.pyc *~ +TEST-*.xml +/foo src/ec_semver_parser.peg diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fcb6c23..0000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: erlang -otp_release: - - 17.0 - - R16B03-1 - - R16B03 - - R16B02 - - R16B01 - - R16B - - R15B02 -script: "./rebar3 compile && ./rebar3 ct" -branches: - only: - - master -notifications: - email: - - core@erlware.org - irc: - channels: - - "irc.freenode.org#erlware" - use_notice: true - skip_join: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e63ac46..cca6505 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ $ git stash pop ``` You SHOULD use these commands both before working on your patch and before -submitting the pull request. If conflicts arise it is your responsability +submitting the pull request. If conflicts arise it is your responsibility to deal with them. You MUST create a new branch for your work. First make sure you have diff --git a/README.md b/README.md index d4e53eb..3e9f193 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ Erlware Commons Current Status -------------- -[![Build Status](https://secure.travis-ci.org/erlware/erlware_commons.png)](http://travis-ci.org/erlware/erlware_commons) + +[![Hex.pm](https://img.shields.io/hexpm/v/erlware_commons)](https://hex.pm/packages/erlware_commons) +[![Tests](https://github.com/erlware/erlware_commons/workflows/EUnit/badge.svg)](https://github.com/erlware/erlware_commons/actions) Introduction ------------ @@ -24,6 +26,18 @@ Goals for the project * Well Documented * Well Tested +Licenses +-------- + +This project contains elements licensed with Apache License, Version 2.0, +as well as elements licensed with The MIT License. + +You'll find license-related information in the header of specific files, +where warranted. + +In cases where no such information is present refer to +[COPYING](COPYING). + Currently Available Modules/Systems ------------------------------------ @@ -56,7 +70,7 @@ href="http://www.erlang.org/doc/man/lists.html">lists, making most list operations parallel. It can operate on each element in parallel, for IO-bound operations, on sublists in parallel, for taking advantage of multi-core machines with CPU-bound operations, and across erlang -nodes, for parallizing inside a cluster. It handles errors and node +nodes, for parallelizing inside a cluster. It handles errors and node failures. It can be configured, tuned, and tweaked to get optimal performance while minimizing overhead. @@ -64,7 +78,7 @@ Almost all the functions are identical to equivalent functions in lists, returning exactly the same result, and having both a form with an identical syntax that operates on each element in parallel and a form which takes an optional "malt", a specification for how to -parallize the operation. +parallelize the operation. fold is the one exception, parallel fold is different from linear fold. This module also include a simple mapreduce implementation, and @@ -76,7 +90,7 @@ runmany, which is as a generalization of parallel list operations. A complete parser for the [semver](http://semver.org/) standard. Including a complete set of conforming comparison functions. -### [ec_lists](https://github.com/ericbmerritt/erlware_commons/blob/master/src/ec_lists.erl) +### [ec_lists](https://github.com/erlware/erlware_commons/blob/master/src/ec_lists.erl) A set of additional list manipulation functions designed to supliment the `lists` module in stdlib. @@ -93,7 +107,7 @@ Other languages, have built in support for **Interface** or **signature** functionality. Java has Interfaces, SML has Signatures. Erlang, though, doesn't currently support this model, at least not directly. There are a few ways you can approximate it. We -have defined a mechnism called *signatures* and several modules that +have defined a mechanism called *signatures* and several modules that to serve as examples and provide a good set of *dictionary* signatures. More information about signatures can be found at [signature](https://github.com/erlware/erlware_commons/blob/master/doc/signatures.md). @@ -110,19 +124,19 @@ This provides an implementation of the ec_dictionary signature using erlang's dicts as a base. The function documentation for ec_dictionary applies here as well. -### [ec_gb_trees](https://github.com/ericbmerritt/erlware_commons/blob/master/src/ec_gb_trees.erl) +### [ec_gb_trees](https://github.com/erlware/erlware_commons/blob/master/src/ec_gb_trees.erl) This provides an implementation of the ec_dictionary signature using erlang's gb_trees as a base. The function documentation for ec_dictionary applies here as well. -### [ec_orddict](https://github.com/ericbmerritt/erlware_commons/blob/master/src/ec_orddict.erl) +### [ec_orddict](https://github.com/erlware/erlware_commons/blob/master/src/ec_orddict.erl) This provides an implementation of the ec_dictionary signature using erlang's orddict as a base. The function documentation for ec_dictionary applies here as well. -### [ec_rbdict](https://github.com/ericbmerritt/erlware_commons/blob/master/src/ec_rbdict.erl) +### [ec_rbdict](https://github.com/erlware/erlware_commons/blob/master/src/ec_rbdict.erl) This provides an implementation of the ec_dictionary signature using Robert Virding's rbdict module as a base. The function documentation diff --git a/doc/property_based_testing.md b/doc/property_based_testing.md deleted file mode 100644 index 414c962..0000000 --- a/doc/property_based_testing.md +++ /dev/null @@ -1,363 +0,0 @@ -Property based testing for unit testers -======================================= - -Main contributors: Torben Hoffmann, Raghav Karol, Eric Merritt - -The purpose of the short document is to help people who are familiar -with unit testing understand how property based testing (PBT) differs, -but also where the thinking is the same. - -This document focusses on the PBT tool -[`PropEr`](https://github.com/manopapad/proper) for Erlang since that is -what I am familiar with, but the general principles applies to all PBT -tools regardless of which language they are written in. - -The approach taken here is that we hear from people who are used to -working with unit testing regarding how they think when designing -their tests and how a concrete test might look. - -These descriptions are then "converted" into the way it works with -PBT, with a clear focus on what stays the same and what is different. - -## Testing philosophies - -### A quote from Martin Logan: - -> For me unit testing is about contracts. I think about the same things -> I think about when I write statements like {ok, Resp} = -> Mod:Func(Args). Unit testing and writing specs are very close for me. -> Hypothetically speaking lets say a function should return return {ok, -> string()} | {error, term()} for all given input parameters then my -> unit tests should be able to show that for a representative set of -> input parameters that those contracts are honored. The art comes in -> thinking about what that set is. - - -The trap in writing all your own tests can often be that we think -about the set in terms of what we coded for and not what may indeed be -asked of our function. As the code is tried in further exploratory -testing and in production new input parameter sets for which the given -function does not meet the stated contract are discovered and added to -the test case once a fix has been put into place. - -This is a very good description of what the ground rules for unit -testing are: - -* Checking that contracts are obeyed. -* Creating a representative set of input parameters. - -The former is very much part of PBT - each property you write will -check a contract, so that thinking is the same. - -## xUnit vs PBT - -Unit testing has become popular for software testing with the advent -of xUnit tools like jUnit for Java. xUnit like tools typically -provide a testing framework with the following functionality - -* test fixture setup -* test case execution -* test fixture teardown -* test suite management -* test status reporting and management - -While xUnit tools provide a lot of functionality to execute and manage -test cases and suites, reporting results there is no focus on test -case execution step, while this is the main focus area of -property-based testing (PBT). - -Consider the following function specification - - :::erlang - sort(list::integer()) ---> list::integer() | error - -A verbal specification of this function is, - -> For all input lists of integers, the sort function returns a sorted -> list of integers. - -For any other kind of argument the function returns the atom error. - -The specification above may be a requirement of how the function -should behave or even how the function does behave. This distinction -is important; the former is the requirement for the function, the -latter is the actual API. Both should be the same and that is what our -testing should confirm. Test cases for this function might look like - - :::erlang - assertEqual(sort([5,4,3,2,1]), [1,2,3,4,5]) - assertEqual(sort([1,2,3,4,5]), [1,2,3,4,5]) - assertEqual(sort([] ), [] ) - assertEqual(sort([-1,0, 1] ), [-1, 0, 1] ) - -How many tests cases should we write to be convinced that the actual -behaviour of the function is the same as its specification? Clearly, -it is impossible to write tests cases for all possible input values, -here all lists of integers, the art of testing is finding individual -input values that are representative of a large part of the input -space. We hope that the test cases are exhaustive to cover the -specification. xUnit tools offer no support for this and this is where -PBT and PBT Tools like `PropEr` and `QuickCheck` come in. - -PBT introduces testing with a large set of random input values and -verifying that the specification holds for each input value -selected. Functions used to generate input values, generators, are -specified using rules and can be simply composed together to construct -complicated values. So, a property based test for the function above -may look like: - - :::erlang - FOREACH({I, J, InputList}, {nat(), nat(), integer_list()}, - SUCHTHAT(I < J andalso J < length(InputList), - SortedList = sort(InputList) - length(SortedList) == length(InputList) - andalso - lists:get(SortedList, I) =< lists:get(SortedList, J)) - - -The property above works as follows - -* Generate a random list of integers `InputList` and two natural numbers - I, J, such that I < J < size of `InputList` -* Check that size of sorted and input lists is the same. -* Check that element with smaller index I is less than or equal to - element with larger index J in `SortedList`. - -Notice in the property above, we *specify* property. Verification of -the property based on random input values will be done by the property -based tool, therefore we can generated a large number of tests cases -with random input values and have a higher level of confidence that -the function when using unit tests alone. - -But it does not stop at generation of input parameters. If you have -more complex tests where you have to generate a series of events and -keep track of some state then your PBT tool will generate random -sequences of events which corresponds to legal sequences of events and -test that your system behaves correctly for all sequences. - -So when you have written a property with associated generators you -have in fact created something that can create numerous test cases - -you just have to tell your PBT tool how many test cases you want to -check the property on. - -## Shrinking the bar - -At this point you might still have the feeling that introducing the -notion of some sort of generators to your unit testing tool of choice -would bring you on par with PBT tools, but wait there is more to -come. - -When a PBT tool creates a test case that fails there is real chance -that it has created a long test case or some big input parameters - -trying to debug that is very much like receiving a humongous log from -a system in the field and try to figure out what cause the system to -fail. - -Enter shrinking... - -When a test case fails the PBT tool will try to shrink the failing -test case down to the essentials by stripping out input elements or -events that does not cause the failure. In most cases this results in -a very short counterexample that clearly states which events and -inputs are required to break a property. - -As we go through some concrete examples later the effects of shrinking -will be shown. - -Shrinking makes it a lot easier to debug problems and is as key to the -strength of PBT as the generators. - -## Converting a unit test - -We will now take a look at one possible way of translating a unit -test into a PBT setting. - -The example comes from Eric Merritt and is about the `add/2` function in -the `ec_dictionary` instance `ec_gb_trees`. - -The add function has the following spec: - - :::erlang - -spec add(ec_dictionary:key(), ec_dictionary:value(), Object::dictionary()) -> - dictionary(). - -and it is supposed to do the obvious: add the key and value pair to -the dictionary and return a new dictionary. - -Eric states his basic expectations as follows: - -1. I can put arbitrary terms into the dictionary as keys -2. I can put arbitrary terms into the dictionary as values -3. When I put a value in the dictionary by a key, I can retrieve that same value -4. When I put a different value in the dictionary by key it does not change other key value pairs. -5. When I update a value the new value in available by the new key -6. When a value does not exist a not found exception is created - -The first two expectations regarding being able to use arbritrary -terms as keys and values is a job for generators. - -The latter four are prime candidates for properties and we will create -one for each of them. - -### Generators - - :::erlang - key() -> any(). - - value() -> any(). - - -For `PropEr` this approach has the drawback that creation and shrinking -becomes rather time consuming, so it might be better to narrow to -something like this: - - :::erlang - key() -> union([integer(),atom()]). - - value() -> union([integer(),atom(),binary(),boolean(),string()]). - -What is best depends on the situation and intended usage. - -Now, being able to generate keys and values is not enough. You also -have to tell `PropEr` how to create a dictionary and in this case we -will use a symbolic generator (detail to be explained later). - - :::erlang - sym_dict() -> - ?SIZED(N,sym_dict(N)). - - sym_dict(0) -> - {'$call',ec_dictionary,new,[ec_gb_trees]}; - sym_dict(N) -> - ?LAZY( - frequency([ - {1, {'$call',ec_dictionary,remove,[key(),sym_dict(N-1)]}}, - {2, {'$call',ec_dictionary,add,[value(),value(),sym_dict(N-1)]}} - ])). - - -`sym_dict/0` uses the `?SIZED` macro to control the size of the -generated dictionary. `PropEr` will start out with small numbers and -gradually raise it. - -`sym_dict/1` is building a dictionary by randomly adding key/value -pairs and removing keys. Eventually the base case is reached which -will create an empty dictionary. - -The `?LAZY` macro is used to defer the calculation of the -`sym_dict(N-1)` until they are needed and `frequency/1` is used -to ensure that twice as many adds compared to removes are done. This -should give rather more interesting dictionaries in the long run, if -not one can alter the frequencies accondingly. - -But does it really work? - -That is a good question and one that should always be asked when -looking at genetors. Fortunately there is a way to see what a -generator produces provided that the generator functions are exported. - -Hint: in most cases it will not hurt to throw in a -`-compile(export_all).` in the module used to specify the -properties. And here we actually have a sub-hint: specify the -properties in a separate file to avoid peeking inside the -implementation! Base the test on the published API as this is what the -users of the code will be restricted to. - -When the test module has been loaded you can test the generators by -starting up an Erlang shell (this example uses the erlware_commons -code so get yourself a clone to play with): - - :::sh - $ erl -pz ebin -pz test - 1> proper_gen:pick(ec_dictionary_proper:key()). - {ok,4} - 2> proper_gen:pick(ec_dictionary_proper:key()). - {ok,35} - 3> proper_gen:pick(ec_dictionary_proper:key()). - {ok,-5} - 4> proper_gen:pick(ec_dictionary_proper:key()). - {ok,48} - 5> proper_gen:pick(ec_dictionary_proper:key()). - {ok,'\036\207_là´?\nc'} - 6> proper_gen:pick(ec_dictionary_proper:value()). - {ok,2} - 7> proper_gen:pick(ec_dictionary_proper:value()). - {ok,-14} - 8> proper_gen:pick(ec_dictionary_proper:value()). - {ok,-3} - 9> proper_gen:pick(ec_dictionary_proper:value()). - {ok,27} - 10> proper_gen:pick(ec_dictionary_proper:value()). - {ok,-8} - 11> proper_gen:pick(ec_dictionary_proper:value()). - {ok,[472765,17121]} - 12> proper_gen:pick(ec_dictionary_proper:value()). - {ok,true} - 13> proper_gen:pick(ec_dictionary_proper:value()). - {ok,<<>>} - 14> proper_gen:pick(ec_dictionary_proper:value()). - {ok,<<89,69,18,148,32,42,238,101>>} - 15> proper_gen:pick(ec_dictionary_proper:sym_dict()). - {ok,{'$call',ec_dictionary,add, - [[114776,1053475], - 'fª\020\227\215', - {'$call',ec_dictionary,add, - ['',true, - {'$call',ec_dictionary,add, - ['2^Ø¡', - [900408,886056], - {'$call',ec_dictionary,add,[[48618|...],<<...>>|...]}]}]}]}} - 16> proper_gen:pick(ec_dictionary_proper:sym_dict()). - {ok,{'$call',ec_dictionary,add, - [10,'a¯\214\031fõC', - {'$call',ec_dictionary,add, - [false,-1, - {'$call',ec_dictionary,remove, - ['d·ÉV÷[', - {'$call',ec_dictionary,remove,[12,{'$call',...}]}]}]}]}} - -That does not look too bad, so we will continue with that for now. - - -### Properties of `add/2` - -The first expectation Eric had about how the dictionary works was that -if a key had been stored it could be retrieved. - -One way of expressing this could be with this property: - - :::erlang - prop_get_after_add_returns_correct_value() -> - ?FORALL({Dict,K,V}, {sym_dict(),key(),value()}, - begin - try ec_dictionary:get(K,ec_dictionary:add(K,V,Dict)) of - V -> - true; - _ -> - false - catch - _:_ -> - false - end - end). - -This property reads that for all dictionaries `get/2` using a key -from a key/value pair just inserted using the `add/3` function -will return that value. If that is not the case the property will -evaluate to false. - -Running the property is done using `proper:quickcheck/1`: - - :::sh - proper:quickcheck(ec_dictionary_proper:prop_get_after_add_returns_correct_value()). - .................................................................................................... - OK: Passed 100 test(s). - true - - -This was as expected, but at this point we will take a little detour -and introduce a mistake in the `ec_gb_trees` implementation and see -how that works. - - - diff --git a/doc/signatures.md b/doc/signatures.md index cf95960..cc5fd32 100644 --- a/doc/signatures.md +++ b/doc/signatures.md @@ -2,10 +2,10 @@ Signatures ========== It often occurs in coding that we need a library, a set of -functionaly. Often there are several algorithms that could provide -this functionality. However, the code that uses it, either doesn't +functionalities. Often there are several algorithms that could provide +each of these functionalities. However, the code that uses it, either doesn't care about the individual algorithm or wishes to delegate choosing -that algorithm to some higher level. Lets take the concrete example of +that algorithm to some higher level. Let's take the concrete example of dictionaries. A dictionary provides the ability to access a value via a key (other things as well but primarily this). There are may ways to implement a dictionary. Just a few are: @@ -16,17 +16,17 @@ implement a dictionary. Just a few are: * [Skip Lists](http://en.wikipedia.org/wiki/Skip_list) * Many, many more .... -Each of these approaches has there own performance characteristics, -memory footprints etc. For example, a table of size n with open -addressing has no collisions and holds up to n elements, with a single -comparison for successful lookup, and a table of size n with chaining -and k keys has the minimum max(0, k-n) collisions and O(1 + k/n) +Each of these approaches has their own performance characteristics, +memory footprints, etc. For example, a table of size $n$ with open +addressing has no collisions and holds up to $n$ elements, with a single +comparison for successful lookup, and a table of size $n$ with chaining +and $k$ keys has the minimum $\max(0, k-n)$ collisions and $\mathcal{O}(1 + k/n)$ comparisons for lookup. While for skip lists the performance characteristics are about as good as that of randomly-built binary -search trees - namely (O log n). So the choice of which to select +search trees - namely $\mathcal{O}(\log n)$. So the choice of which to select depends very much on memory available, insert/read characteristics, etc. So delegating the choice to a single point in your code is a very -good idea. Unfortunately, in Erlang thats ot so easy to do at the moment. +good idea. Unfortunately, in Erlang that's so easy to do at the moment. Other languages, have built in support for this functionality. [Java](http://en.wikipedia.org/wiki/Java_(programming_language)) @@ -39,17 +39,20 @@ directly. There are a few ways you can approximate it. One way is to pass the Module name to the calling functions along with the data that it is going to be called on. - :::erlang - add(ModuleToUse, Key, Value, DictData) -> - ModuleToUse:add(Key, Value, DictData). +```erlang +add(ModuleToUse, Key, Value, DictData) -> + ModuleToUse:add(Key, Value, DictData). +``` This works, and you can vary how you want to pass the data. For example, you could easily use a tuple to contain the data. That is, you could pass in `{ModuleToUse, DictData}` and that would make it a bit cleaner. - :::erlang - add(Key, Value, {ModuleToUse, DictData}) -> - ModuleToUse:add(Key, Value, DictData). + +```erlang +add(Key, Value, {ModuleToUse, DictData}) -> + ModuleToUse:add(Key, Value, DictData). +``` Either way, there are a few problems with this approach. One of the biggest is that you lose code locality, by looking at this bit of code @@ -63,21 +66,22 @@ mistakes that you might have made. Tools like [Dialyzer](http://www.erlang.org/doc/man/dialyzer.html) have just as hard a time figuring out the what `ModuleToUse` is pointing to as you do. So they can't give you warnings about potential problems. In fact -someone could inadvertantly pass an unexpected function name as +someone could inadvertently pass an unexpected function name as `ModuleToUse` and you would never get any warnings, just an exception at run time. -Fortunately, Erlang is a pretty flexable language so we can use a +Fortunately, Erlang is a pretty flexible language so we can use a similar approach with a few adjustments to give us the best of both -worlds. Both the flexibiltiy of ignoreing a specific implementation +worlds. Both the flexibility of ignoring a specific implementation and keeping all the nice locality we get by using an explicit module name. So what we actually want to do is something mole like this: - :::erlang - add(Key, Value, DictData) -> - dictionary:add(Key, Value, DictData). +```erlang +add(Key, Value, DictData) -> + dictionary:add(Key, Value, DictData). +``` Doing this we retain the locality. We can easily look up the `dictionary` Module. We immediately have a good idea what a @@ -90,54 +94,56 @@ reasons, this is a much better approach to the problem. This is what Signatures ---------- -How do we actually do this in Erlang now that Erlang is missing what Java, SML and friends has built in? +How do we actually do this in Erlang now that Erlang is missing what Java, SML and friends have built in? The first thing we need to do is to define a [Behaviour](http://metajack.im/2008/10/29/custom-behaviors-in-erlang/) for our functionality. To continue our example we will define a Behaviour for dictionaries. That Behaviour looks like this: - :::erlang - -module(ec_dictionary). +```erlang +-module(ec_dictionary). - -export([behaviour_info/1]). +-export([behaviour_info/1]). - behaviour_info(callbacks) -> - [{new, 0}, - {has_key, 2}, - {get, 2}, - {add, 3}, - {remove, 2}, - {has_value, 2}, - {size, 1}, - {to_list, 1}, - {from_list, 1}, - {keys, 1}]; - behaviour_info(_) -> - undefined. +behaviour_info(callbacks) -> + [{new, 0}, + {has_key, 2}, + {get, 2}, + {add, 3}, + {remove, 2}, + {has_value, 2}, + {size, 1}, + {to_list, 1}, + {from_list, 1}, + {keys, 1}]; +behaviour_info(_) -> + undefined. +``` So we have our Behaviour now. Unfortunately, this doesn't give us much yet. It will make sure that any dictionaries we write will have all -the functions they need to have, but it wont help use actually use the +the functions they need to have, but it won't help us actually use the dictionaries in an abstract way in our code. To do that we need to add a bit of functionality. We do that by actually implementing our own behaviour, starting with `new/1`. - :::erlang - %% @doc create a new dictionary object from the specified module. The - %% module should implement the dictionary behaviour. - %% - %% @param ModuleName The module name. - -spec new(module()) -> dictionary(_K, _V). - new(ModuleName) when is_atom(ModuleName) -> - #dict_t{callback = ModuleName, data = ModuleName:new()}. +```erlang +%% @doc create a new dictionary object from the specified module. The +%% module should implement the dictionary behaviour. +%% +%% @param ModuleName The module name. +-spec new(module()) -> dictionary(_K, _V). +new(ModuleName) when is_atom(ModuleName) -> + #dict_t{callback = ModuleName, data = ModuleName:new()}. +``` This code creates a new dictionary for us. Or to be more specific it actually creates a new dictionary Signature record, that will be used subsequently in other calls. This might look a bit familiar from our previous less optimal approach. We have both the module name and the -data. here in the record. We call the module name named in +data in the record. We call the module name named in `ModuleName` to create the initial data. We then construct the record and return that record to the caller and we have a new dictionary. What about the other functions, the ones that don't create @@ -148,16 +154,17 @@ dictionary and another that just retrieves data. The first we will look at is the one that updates the dictionary by adding a value. - :::erlang - %% @doc add a new value to the existing dictionary. Return a new - %% dictionary containing the value. - %% - %% @param Dict the dictionary object to add too - %% @param Key the key to add - %% @param Value the value to add - -spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V). - add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) -> - Dict#dict_t{data = Mod:add(Key, Value, Data)}. +```erlang +%% @doc add a new value to the existing dictionary. Return a new +%% dictionary containing the value. +%% +%% @param Dict the dictionary object to add too +%% @param Key the key to add +%% @param Value the value to add +-spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V). +add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) -> + Dict#dict_t{data = Mod:add(Key, Value, Data)}. +``` There are two key things here. @@ -173,16 +180,17 @@ implementation to do the work itself. Now lets do a data retrieval function. In this case, the `get` function of the dictionary Signature. - :::erlang - %% @doc given a key return that key from the dictionary. If the key is - %% not found throw a 'not_found' exception. - %% - %% @param Dict The dictionary object to return the value from - %% @param Key The key requested - %% @throws not_found when the key does not exist - -spec get(key(K), dictionary(K, V)) -> value(V). - get(Key, #dict_t{callback = Mod, data = Data}) -> - Mod:get(Key, Data). +```erlang +%% @doc given a key return that key from the dictionary. If the key is +%% not found throw a 'not_found' exception. +%% +%% @param Dict The dictionary object to return the value from +%% @param Key The key requested +%% @throws not_found when the key does not exist +-spec get(key(K), dictionary(K, V)) -> value(V). +get(Key, #dict_t{callback = Mod, data = Data}) -> + Mod:get(Key, Data). +``` In this case, you can see a very similar approach to deconstructing the dict record. We still need to pull out the callback module and the @@ -197,7 +205,7 @@ implementation in Using Signatures ---------------- -Its a good idea to work through an example so we have a bit better +It's a good idea to work through an example so we have a bit better idea of how to use these Signatures. If you are like me, you probably have some questions about what kind of performance burden this places on the code. At the very least we have an additional function call @@ -206,7 +214,7 @@ lets write a little timing test, so we can get a good idea of how much this is all costing us. In general, there are two kinds of concrete implementations for -Signatures. The first is a native implementations, the second is a +Signatures. The first is a native implementation, the second is a wrapper. ### Native Signature Implementations @@ -223,32 +231,33 @@ implements the ec_dictionary module directly. A Signature Wrapper is a module that wraps another module. Its purpose is to help a preexisting module implement the Behaviour -defined by a Signature. A good example if this in our current example +defined by a Signature. A good example of this in our current example is the [erlware_commons/ec_dict](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dict.erl) -module. It implements the ec_dictionary Behaviour, but all the +module. It implements the `ec_dictionary` Behaviour, but all the functionality is provided by the [stdlib/dict](http://www.erlang.org/doc/man/dict.html) module -itself. Lets take a look at one example to see how this is done. +itself. Let's take a look at one example to see how this is done. We will take a look at one of the functions we have already seen. The -`get` function an ec_dictionary `get` doesn't have quite the same -semantics as any of the functions in the dict module. So a bit of -translation needs to be done. We do that in the ec_dict module `get` function. +`get` function in `ec_dictionary` doesn't have quite the same +semantics as any of the functions in the `dict` module. So a bit of +translation needs to be done. We do that in the `ec_dict:get/2` function. - :::erlang - -spec get(ec_dictionary:key(K), Object::dictionary(K, V)) -> - ec_dictionary:value(V). - get(Key, Data) -> - case dict:find(Key, Data) of - {ok, Value} -> - Value; - error -> - throw(not_found) - end. +```erlang +-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) -> + ec_dictionary:value(V). +get(Key, Data) -> + case dict:find(Key, Data) of + {ok, Value} -> + Value; + error -> + throw(not_found) + end. +``` -So the ec_dict module's purpose for existence is to help the -preexisting dict module implement the Behaviour defined by the +So the `ec_dict` module's purpose for existence is to help the +preexisting `dict` module implement the Behaviour defined by the Signature. @@ -258,24 +267,25 @@ the mix and that adds a bit of additional overhead. ### Creating the Timing Module -We are going to creating timings for both Native Signature +We are going to be creating timings for both Native Signature Implementations and Signature Wrappers. -Lets get started by looking at some helper functions. We want -dictionaries to have a bit of data in them. So to that end we are will +Let's get started by looking at some helper functions. We want +dictionaries to have a bit of data in them. So to that end we will create a couple of functions that create dictionaries for each type we want to test. The first we want to time is the Signature Wrapper, so `dict` vs `ec_dict` called as a Signature. - :::erlang - create_dict() -> +```erlang +create_dict() -> lists:foldl(fun(El, Dict) -> - dict:store(El, El, Dict) - end, dict:new(), - lists:seq(1,100)). + dict:store(El, El, Dict) + end, dict:new(), + lists:seq(1,100)). +``` The only thing we do here is create a sequence of numbers 1 to 100, -and then add each of those to the dict as an entry. We aren't too +and then add each of those to the `dict` as an entry. We aren't too worried about replicating real data in the dictionary. We care about timing the function call overhead of Signatures, not the performance of the dictionaries themselves. @@ -283,58 +293,61 @@ of the dictionaries themselves. We need to create a similar function for our Signature based dictionary `ec_dict`. - :::erlang - create_dictionary(Type) -> +```erlang +create_dictionary(Type) -> lists:foldl(fun(El, Dict) -> - ec_dictionary:add(El, El, Dict) - end, - ec_dictionary:new(Type), - lists:seq(1,100)). + ec_dictionary:add(El, El, Dict) + end, + ec_dictionary:new(Type), + lists:seq(1,100)). +``` Here we actually create everything using the Signature. So we don't need one function for each type. We can have one function that can create anything that implements the Signature. That is the magic of -Signatures. Otherwise, this does the exact same thing as the dict -`create_dict/1`. +Signatures. Otherwise, this does the exact same thing as the dictionary +given by `create_dict/0`. We are going to use two function calls in our timing. One that updates data and one that returns data, just to get good coverage. For our -dictionaries that we are going to use the `size` function as well as +dictionaries we are going to use the `size` function as well as the `add` function. - :::erlang - time_direct_vs_signature_dict() -> - io:format("Timing dict~n"), - Dict = create_dict(), - test_avg(fun() -> - dict:size(dict:store(some_key, some_value, Dict)) - end, - 1000000), - io:format("Timing ec_dict implementation of ec_dictionary~n"), - time_dict_type(ec_dict). +```erlang +time_direct_vs_signature_dict() -> + io:format("Timing dict~n"), + Dict = create_dict(), + test_avg(fun() -> + dict:size(dict:store(some_key, some_value, Dict)) + end, + 1000000), + io:format("Timing ec_dict implementation of ec_dictionary~n"), + time_dict_type(ec_dict). +``` The `test_avg` function runs the provided function the number of times specified in the second argument and collects timing information. We -are going to run these one million times to get a good average (its -fast so it doesn't take long). You can see that in the anonymous +are going to run these one million times to get a good average (it's +fast so it doesn't take long). You can see in the anonymous function that we directly call `dict:size/1` and `dict:store/3` to perform the test. However, because we are in the wonderful world of Signatures we don't have to hard code the calls for the Signature implementations. Lets take a look at the `time_dict_type` function. - :::erlang - time_dict_type(Type) -> - io:format("Testing ~p~n", [Type]), - Dict = create_dictionary(Type), - test_avg(fun() -> - ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict)) - end, - 1000000). +```erlang +time_dict_type(Type) -> + io:format("Testing ~p~n", [Type]), + Dict = create_dictionary(Type), + test_avg(fun() -> + ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict)) + end, + 1000000). +``` As you can see we take the type as an argument (we need it for `dict` creation) and call our create function. Then we run the same timings -that we did for ec dict. In this case though, the type of dictionary +that we did for `ec_dict`. In this case though, the type of dictionary is never specified, we only ever call ec_dictionary, so this test will work for anything that implements that Signature. @@ -343,25 +356,26 @@ work for anything that implements that Signature. So we have our tests, what was the result. Well on my laptop this is what it looked like. - :::sh - Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] +```sh +Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] - Eshell V5.8.2 (abort with ^G) +Eshell V5.8.2 (abort with ^G) - 1> ec_timing:time_direct_vs_signature_dict(). - Timing dict - Range: 2 - 5621 mics - Median: 3 mics - Average: 3 mics - Timing ec_dict implementation of ec_dictionary - Testing ec_dict - Range: 3 - 6097 mics - Median: 3 mics - Average: 4 mics - 2> +1> ec_timing:time_direct_vs_signature_dict(). +Timing dict +Range: 2 - 5621 mics +Median: 3 mics +Average: 3 mics +Timing ec_dict implementation of ec_dictionary +Testing ec_dict +Range: 3 - 6097 mics +Median: 3 mics +Average: 4 mics +2> +``` -So for the direct dict call, we average about 3 mics per call, while -for the Signature Wrapper we average around 4. Thats a 25% cost for +So for the direct `dict` call, we average about 3 mics per call, while +for the Signature Wrapper we average around 4. That's a 25% cost for Signature Wrappers in this example, for a very small number of calls. Depending on what you are doing that is going to be greater or lesser. In any case, we can see that there is some cost associated @@ -373,30 +387,32 @@ Signature, but it is not a Signature Wrapper. It is a native implementation of the Signature. To use `ec_rbdict` directly we have to create a creation helper just like we did for dict. - :::erlang - create_rbdict() -> +```erlang +create_rbdict() -> lists:foldl(fun(El, Dict) -> - ec_rbdict:add(El, El, Dict) - end, ec_rbdict:new(), - lists:seq(1,100)). + ec_rbdict:add(El, El, Dict) + end, ec_rbdict:new(), + lists:seq(1,100)). +``` This is exactly the same as `create_dict` with the exception that dict is replaced by `ec_rbdict`. The timing function itself looks very similar as well. Again notice that we have to hard code the concrete name for the concrete -implementation, but we don't for the ec_dictionary test. +implementation, but we don't for the `ec_dictionary` test. - :::erlang - time_direct_vs_signature_rbdict() -> - io:format("Timing rbdict~n"), - Dict = create_rbdict(), - test_avg(fun() -> - ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict)) - end, - 1000000), - io:format("Timing ec_dict implementation of ec_dictionary~n"), - time_dict_type(ec_rbdict). +```erlang +time_direct_vs_signature_rbdict() -> + io:format("Timing rbdict~n"), + Dict = create_rbdict(), + test_avg(fun() -> + ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict)) + end, + 1000000), + io:format("Timing ec_dict implementation of ec_dictionary~n"), + time_dict_type(ec_rbdict). +``` And there we have our test. What do the results look like? @@ -406,34 +422,35 @@ The main thing we are timing here is the additional cost of the dictionary Signature itself. Keep that in mind as we look at the results. - :::sh - Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] +```sh +Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] - Eshell V5.8.2 (abort with ^G) +Eshell V5.8.2 (abort with ^G) - 1> ec_timing:time_direct_vs_signature_rbdict(). - Timing rbdict - Range: 6 - 15070 mics - Median: 7 mics - Average: 7 mics - Timing ec_dict implementation of ec_dictionary - Testing ec_rbdict - Range: 6 - 6013 mics - Median: 7 mics - Average: 7 mics - 2> +1> ec_timing:time_direct_vs_signature_rbdict(). +Timing rbdict +Range: 6 - 15070 mics +Median: 7 mics +Average: 7 mics +Timing ec_dict implementation of ec_dictionary +Testing ec_rbdict +Range: 6 - 6013 mics +Median: 7 mics +Average: 7 mics +2> +``` So no difference it time. Well the reality is that there is a difference in timing, there must be, but we don't have enough resolution in the timing system to be able to figure out what that -difference is. Essentially that means its really, really small - or small +difference is. Essentially that means it's really, really small - or small enough not to worry about at the very least. Conclusion ---------- Signatures are a viable, useful approach to the problem of interfaces -in Erlang. The have little or no over head depending on the type of +in Erlang. They have little or no overhead depending on the type of implementation, and greatly increase the flexibility of the a library while retaining testability and locality. @@ -456,7 +473,7 @@ Signature Wrapper ### Code Referenced -* [ec_dictionary Implementation] (https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dictionary.erl) -* [ec_dict Signature Wrapper] (https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dict.erl) -* [ec_rbdict Native Signature Implementation] (https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_rbdict.erl) -* [ec_timing Signature Use Example and Timing Collector] (https://github.com/ericbmerritt/erlware_commons/blob/types/examples/ec_timing.erl) +* [ec_dictionary Implementation](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dictionary.erl) +* [ec_dict Signature Wrapper](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dict.erl) +* [ec_rbdict Native Signature Implementation](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_rbdict.erl) +* [ec_timing Signature Use Example and Timing Collector](https://github.com/ericbmerritt/erlware_commons/blob/types/examples/ec_timing.erl) diff --git a/priv/ec_semver_parser.peg b/priv/ec_semver_parser.peg index 1210ff9..d0444a6 100644 --- a/priv/ec_semver_parser.peg +++ b/priv/ec_semver_parser.peg @@ -7,8 +7,3 @@ version_part <- numeric_part / alpha_part ; numeric_part <- [0-9]+ `erlang:list_to_integer(erlang:binary_to_list(erlang:iolist_to_binary(Node)))` ; alpha_part <- [A-Za-z0-9]+ `erlang:iolist_to_binary(Node)` ; - -%% This only exists to get around a bug in erlang where if -%% warnings_as_errors is specified `nowarn` directives are ignored - - `-compile(export_all).` \ No newline at end of file diff --git a/rebar.config b/rebar.config index 48d5b07..017e3fe 100644 --- a/rebar.config +++ b/rebar.config @@ -1,17 +1,14 @@ %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- %% Dependencies ================================================================ -{deps, []}. +{deps, [ + {cf, "~>0.3"} +]}. {erl_first_files, ["ec_dictionary", "ec_vsn"]}. %% Compiler Options ============================================================ -{erl_opts, - [{platform_define, "^[0-9]+", namespaced_types}, - {platform_define, "^[0-9]+", have_callback_support}, - {platform_define, "^R1[4|5]", deprecated_crypto}, - debug_info, - warnings_as_errors]}. +{erl_opts, [debug_info, warnings_as_errors]}. %% EUnit ======================================================================= {eunit_opts, [verbose, @@ -21,8 +18,7 @@ {cover_print_enabled, true}. %% Profiles ==================================================================== -{profiles, [{dev, [{deps, [{neotoma, "", - {git, "https://github.com/seancribbs/neotoma.git", {branch, master}}}, - {proper, "", - {git, "https://github.com/bkearns/proper.git", {branch, master}}}]}]} +{profiles, [{dev, [{deps, + [{neotoma, "", + {git, "https://github.com/seancribbs/neotoma.git", {branch, master}}}]}]} ]}. diff --git a/rebar.config.script b/rebar.config.script new file mode 100644 index 0000000..cc054a8 --- /dev/null +++ b/rebar.config.script @@ -0,0 +1,7 @@ +NoDialWarns = {dialyzer, [{warnings, [no_unknown]}]}, +OTPRelease = erlang:list_to_integer(erlang:system_info(otp_release)), + +case OTPRelease<26 of + true -> CONFIG; + false -> lists:keystore(dialyzer, 1, CONFIG, NoDialWarns) +end. diff --git a/rebar.lock b/rebar.lock index 57afcca..7873d25 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1 +1,8 @@ -[]. +{"1.2.0", +[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},0}]}. +[ +{pkg_hash,[ + {<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>}]}, +{pkg_hash_ext,[ + {<<"cf">>, <<"315E8D447D3A4B02BCDBFA397AD03BBB988A6E0AA6F44D3ADD0F4E3C3BF97672">>}]} +]. diff --git a/rebar3 b/rebar3 index c44f808..a5f263e 100755 Binary files a/rebar3 and b/rebar3 differ diff --git a/src/ec_cmd_log.erl b/src/ec_cmd_log.erl index 4c54086..a1f2713 100644 --- a/src/ec_cmd_log.erl +++ b/src/ec_cmd_log.erl @@ -19,11 +19,15 @@ %%% @copyright (C) 2012 Erlware, LLC. %%% %%% @doc This provides simple output functions for command line apps. You should -%%% use this to talk to the users if you are wrting code for the system +%%% use this to talk to the users if you are writing code for the system -module(ec_cmd_log). +%% Avoid clashing with `error/3` BIF added in Erlang/OTP 24 +-compile({no_auto_import,[error/3]}). + -export([new/1, new/2, + new/3, log/4, should/2, debug/2, @@ -36,22 +40,17 @@ warn/3, log_level/1, atom_log_level/1, + colorize/4, format/1]). --include("ec_cmd_log.hrl"). - --define(RED, 31). --define(GREEN, 32). --define(YELLOW, 33). --define(BLUE, 34). --define(MAGENTA, 35). --define(CYAN, 36). +-include("include/ec_cmd_log.hrl"). +-include("src/ec_cmd_log.hrl"). -define(PREFIX, "===> "). -record(state_t, {log_level=0 :: int_log_level(), caller=api :: caller(), - term_cap=full :: full | dumb }). + intensity=low :: intensity()}). %%============================================================================ %% types @@ -71,9 +70,11 @@ -type atom_log_level() :: error | warn | info | debug. +-type intensity() :: none | low | high. + -type log_fun() :: fun(() -> iolist()). --type color() :: 31..36. +-type color() :: char(). -opaque t() :: #state_t{}. @@ -86,9 +87,18 @@ new(LogLevel) -> new(LogLevel, api). -spec new(log_level(), caller()) -> t(). -new(LogLevel, Caller) when LogLevel >= 0, LogLevel =< 3 -> - #state_t{log_level=LogLevel, caller=Caller, term_cap=query_term_env()}; -new(AtomLogLevel, Caller) +new(LogLevel, Caller) -> + new(LogLevel, Caller, high). + + +-spec new(log_level(), caller(), intensity()) -> t(). +new(LogLevel, Caller, Intensity) when (Intensity =:= none orelse + Intensity =:= low orelse + Intensity =:= high), + LogLevel >= 0, LogLevel =< 3 -> + #state_t{log_level=LogLevel, caller=Caller, + intensity=Intensity}; +new(AtomLogLevel, Caller, Intensity) when AtomLogLevel =:= error; AtomLogLevel =:= warn; AtomLogLevel =:= info; @@ -99,7 +109,8 @@ new(AtomLogLevel, Caller) info -> 2; debug -> 3 end, - new(LogLevel, Caller). + new(LogLevel, Caller, Intensity). + %% @doc log at the debug level given the current log state with a string or %% function that returns a string @@ -110,10 +121,10 @@ debug(LogState, Fun) colorize(LogState, ?CYAN, false, Fun()) end); debug(LogState, String) -> - debug(LogState, "~s~n", [String]). + debug(LogState, "~ts~n", [String]). %% @doc log at the debug level given the current log state with a format string -%% and argements @see io:format/2 +%% and arguments @see io:format/2 -spec debug(t(), string(), [any()]) -> ok. debug(LogState, FormatString, Args) -> log(LogState, ?EC_DEBUG, colorize(LogState, ?CYAN, false, FormatString), Args). @@ -127,10 +138,10 @@ info(LogState, Fun) colorize(LogState, ?GREEN, false, Fun()) end); info(LogState, String) -> - info(LogState, "~s~n", [String]). + info(LogState, "~ts~n", [String]). %% @doc log at the info level given the current log state with a format string -%% and argements @see io:format/2 +%% and arguments @see io:format/2 -spec info(t(), string(), [any()]) -> ok. info(LogState, FormatString, Args) -> log(LogState, ?EC_INFO, colorize(LogState, ?GREEN, false, FormatString), Args). @@ -144,10 +155,10 @@ error(LogState, Fun) colorize(LogState, ?RED, false, Fun()) end); error(LogState, String) -> - error(LogState, "~s~n", [String]). + error(LogState, "~ts~n", [String]). %% @doc log at the error level given the current log state with a format string -%% and argements @see io:format/2 +%% and arguments @see io:format/2 -spec error(t(), string(), [any()]) -> ok. error(LogState, FormatString, Args) -> log(LogState, ?EC_ERROR, colorize(LogState, ?RED, false, FormatString), Args). @@ -159,10 +170,10 @@ warn(LogState, Fun) when erlang:is_function(Fun) -> log(LogState, ?EC_WARN, fun() -> colorize(LogState, ?MAGENTA, false, Fun()) end); warn(LogState, String) -> - warn(LogState, "~s~n", [String]). + warn(LogState, "~ts~n", [String]). %% @doc log at the warn level given the current log state with a format string -%% and argements @see io:format/2 +%% and arguments @see io:format/2 -spec warn(t(), string(), [any()]) -> ok. warn(LogState, FormatString, Args) -> log(LogState, ?EC_WARN, colorize(LogState, ?MAGENTA, false, FormatString), Args). @@ -171,7 +182,7 @@ warn(LogState, FormatString, Args) -> -spec log(t(), int_log_level(), log_fun()) -> ok. log(#state_t{log_level=DetailLogLevel}, LogLevel, Fun) when DetailLogLevel >= LogLevel -> - io:format("~s~n", [Fun()]); + io:format("~ts~n", [Fun()]); log(_, _, _) -> ok. @@ -218,54 +229,29 @@ format(Log) -> <<")">>]. -spec colorize(t(), color(), boolean(), string()) -> string(). -colorize(#state_t{caller=command_line, term_cap=full}, Color, false, Msg) when is_integer(Color) -> - lists:flatten(io_lib:format("\033[~B;~Bm~s~s\033[0m", [0, Color, ?PREFIX, Msg])); -colorize(#state_t{caller=command_line, term_cap=dumb}, Color, _Bold, Msg) when is_integer(Color) -> - lists:flatten(io_lib:format("~s~s", [?PREFIX, Msg])); + +-define(VALID_COLOR(C), + C =:= $r orelse C =:= $g orelse C =:= $y orelse + C =:= $b orelse C =:= $m orelse C =:= $c orelse + C =:= $R orelse C =:= $G orelse C =:= $Y orelse + C =:= $B orelse C =:= $M orelse C =:= $C). + +colorize(#state_t{intensity=none}, _, _, Msg) -> + Msg; +%% When it is supposed to be bold and we already have a uppercase +%% (bold color) we don't need to modify the color +colorize(State, Color, true, Msg) when ?VALID_COLOR(Color), + Color >= $A, Color =< $Z -> + colorize(State, Color, false, Msg); +%% We're sneaky we can subtract 32 to get the uppercase character if we want +%% bold but have a non bold color. +colorize(State, Color, true, Msg) when ?VALID_COLOR(Color) -> + colorize(State, Color - 32, false, Msg); +colorize(#state_t{caller=command_line, intensity = high}, + Color, false, Msg) when ?VALID_COLOR(Color) -> + lists:flatten(cf:format("~!" ++ [Color] ++"~ts~ts", [?PREFIX, Msg])); +colorize(#state_t{caller=command_line, intensity = low}, + Color, false, Msg) when ?VALID_COLOR(Color) -> + lists:flatten(cf:format("~!" ++ [Color] ++"~ts~!!~ts", [?PREFIX, Msg])); colorize(_LogState, _Color, _Bold, Msg) -> Msg. - -%% @doc Query the term enviroment -%% For reasons of simplicity, we don't parse terminal capabilities yet, although -%% a later version could do so. Rather, we provide a simple match-list of terminal -%% capabilities. -%% @end --spec query_term_env() -> full | dumb. -query_term_env() -> - term_capabilities(os:getenv("TERM")). - --spec term_capabilities(string()) -> full | dumb. -term_capabilities("xterm") -> full; -term_capabilities("dumb") -> dumb; -term_capabilities(_) -> full. %% Default to the backwards compatible version. - -%%%=================================================================== -%%% Test Functions -%%%=================================================================== - --ifdef(DEV_ONLY). --include_lib("eunit/include/eunit.hrl"). - -should_test() -> - ErrorLogState = new(error), - ?assertMatch(true, should(ErrorLogState, ?EC_ERROR)), - ?assertMatch(true, not should(ErrorLogState, ?EC_INFO)), - ?assertMatch(true, not should(ErrorLogState, ?EC_DEBUG)), - ?assertEqual(?EC_ERROR, log_level(ErrorLogState)), - ?assertEqual(error, atom_log_level(ErrorLogState)), - - InfoLogState = new(info), - ?assertMatch(true, should(InfoLogState, ?EC_ERROR)), - ?assertMatch(true, should(InfoLogState, ?EC_INFO)), - ?assertMatch(true, not should(InfoLogState, ?EC_DEBUG)), - ?assertEqual(?EC_INFO, log_level(InfoLogState)), - ?assertEqual(info, atom_log_level(InfoLogState)), - - DebugLogState = new(debug), - ?assertMatch(true, should(DebugLogState, ?EC_ERROR)), - ?assertMatch(true, should(DebugLogState, ?EC_INFO)), - ?assertMatch(true, should(DebugLogState, ?EC_DEBUG)), - ?assertEqual(?EC_DEBUG, log_level(DebugLogState)), - ?assertEqual(debug, atom_log_level(DebugLogState)). - --endif. diff --git a/src/ec_cmd_log.hrl b/src/ec_cmd_log.hrl new file mode 100644 index 0000000..428fd74 --- /dev/null +++ b/src/ec_cmd_log.hrl @@ -0,0 +1,7 @@ +%%% @copyright 2024 Erlware, LLC. +-define(RED, $r). +-define(GREEN, $g). +-define(YELLOW, $y). +-define(BLUE, $b). +-define(MAGENTA, $m). +-define(CYAN, $c). diff --git a/src/ec_cnv.erl b/src/ec_cnv.erl index 80b05e7..bc3b3f3 100644 --- a/src/ec_cnv.erl +++ b/src/ec_cnv.erl @@ -33,10 +33,6 @@ is_true/1, is_false/1]). --ifdef(DEV_ONLY). --include_lib("proper/include/proper.hrl"). --endif. - %%%=================================================================== %%% API %%%=================================================================== @@ -216,94 +212,3 @@ to_atom(X) erlang:list_to_existing_atom(X); to_atom(X) -> to_atom(to_list(X)). - -%%%=================================================================== -%%% Tests -%%%=================================================================== - --ifdef(DEV_ONLY). --include_lib("eunit/include/eunit.hrl"). - -force_proper_test_() -> - {"Runs PropEr test during EUnit phase", - {timeout, 15000, [?_assertEqual([], proper:module(?MODULE))]}}. - -to_integer_test() -> - ?assertError(badarg, to_integer(1.5, strict)). - -to_float_test() -> - ?assertError(badarg, to_float(10, strict)). - -to_atom_test() -> - ?assertMatch(true, to_atom("true")), - ?assertMatch(true, to_atom(<<"true">>)), - ?assertMatch(false, to_atom(<<"false">>)), - ?assertMatch(false, to_atom(false)), - ?assertError(badarg, to_atom("hello_foo_bar_baz")), - - S = erlang:list_to_atom("1"), - ?assertMatch(S, to_atom(1)). - -to_boolean_test()-> - ?assertMatch(true, to_boolean(<<"true">>)), - ?assertMatch(true, to_boolean("true")), - ?assertMatch(true, to_boolean(true)), - ?assertMatch(false, to_boolean(<<"false">>)), - ?assertMatch(false, to_boolean("false")), - ?assertMatch(false, to_boolean(false)). - -%%% PropEr testing - -prop_to_integer() -> - ?FORALL({F, I}, {float(), integer()}, - begin - Is = [[Fun(N), N] || - Fun <- [fun to_list/1, - fun to_binary/1], - N <- [F, I]], - lists:all(fun([FN, N]) -> - erlang:is_integer(to_integer(N)) andalso - erlang:is_integer(to_integer(FN)) - end, Is) - end). - -prop_to_list() -> - ?FORALL({A, L, B, I, F}, {atom(), list(), binary(), integer(), float()}, - lists:all(fun(X) -> - erlang:is_list(to_list(X)) - end, [A, L, B, I, F])). - -prop_to_binary() -> - ?FORALL({A, L, B, I, F, IO}, {atom(), list(range(0,255)), binary(), - integer(), float(), iolist()}, - lists:all(fun(X) -> - erlang:is_binary(to_binary(X)) - end, [A, L, B, I, F, IO])). - -prop_iolist_t() -> - ?FORALL(IO, iolist(), erlang:is_binary(to_binary(IO))). - -prop_to_float() -> - ?FORALL({F, I}, {float(), integer()}, - begin - Fs = [[Fun(N), N] || - Fun <- [fun to_list/1, fun to_binary/1], - N <- [F, I]], - lists:all(fun([FN, N]) -> - erlang:is_float(to_float(N)) andalso - erlang:is_float(to_float(FN)) - end, Fs) - end). - -prop_to_number() -> - ?FORALL({F, I}, {float(), integer()}, - begin - Is = [[Fun(N), N] || - Fun <- [fun to_list/1, fun to_binary/1], - N <- [F, I] ], - lists:all(fun([FN, N]) -> - erlang:is_number(to_number(N)) andalso - erlang:is_number(to_number(FN)) - end, Is) - end). --endif. diff --git a/src/ec_compile.erl b/src/ec_compile.erl index 6c15520..7199610 100644 --- a/src/ec_compile.erl +++ b/src/ec_compile.erl @@ -38,7 +38,7 @@ beam_to_erl_source(BeamFName, ErlFName) -> Src = erl_prettypr:format(erl_syntax:form_list(tl(Forms))), {ok, Fd} = file:open(ErlFName, [write]), - io:fwrite(Fd, "~s~n", [Src]), + io:fwrite(Fd, "~ts~n", [Src]), file:close(Fd); Error -> Error diff --git a/src/ec_date.erl b/src/ec_date.erl index f04e399..747b246 100644 --- a/src/ec_date.erl +++ b/src/ec_date.erl @@ -44,9 +44,9 @@ -define( is_month(X), ( (is_integer(X) andalso X =< 12) orelse ?is_hinted_month(X) ) ). -define( is_tz_offset(H1,H2,M1,M2), (?is_num(H1) andalso ?is_num(H2) andalso ?is_num(M1) andalso ?is_num(M2)) ). --define(GREGORIAN_SECONDS_1970, 62167219200). --define(ISO_8601_DATETIME_FORMAT, "Y-m-dTG:i:sZ"). --define(ISO_8601_DATETIME_WITH_MS_FORMAT, "Y-m-dTG:i:s.fZ"). +-define(GREGORIAN_SECONDS_1970, 62_167_219_200). +-define(ISO_8601_DATETIME_FORMAT, "Y-m-dTH:i:sZ"). +-define(ISO_8601_DATETIME_WITH_MS_FORMAT, "Y-m-dTH:i:s.fZ"). -type year() :: non_neg_integer(). -type month() :: 1..12 | {?MONTH_TAG, 1..12}. @@ -54,11 +54,11 @@ -type hour() :: 0..23. -type minute() :: 0..59. -type second() :: 0..59. --type microsecond() :: 0..1000000. +-type microsecond() :: 0..999_999. -type daynum() :: 1..7. -type date() :: {year(),month(),day()}. --type time() :: {hour(),minute(),second()} |{hour(),minute(),second(), microsecond()}. +-type time() :: {hour(),minute(),second()} | {hour(),minute(),second(),microsecond()}. -type datetime() :: {date(),time()}. -type now() :: {integer(),integer(),integer()}. @@ -101,7 +101,7 @@ parse(Date, Now) -> do_parse(Date, Now, []). do_parse(Date, Now, Opts) -> - case filter_hints(parse(tokenise(string:to_upper(Date), []), Now, Opts)) of + case filter_hints(parse(tokenise(string:uppercase(Date), []), Now, Opts)) of {error, bad_date} -> erlang:throw({?MODULE, {bad_date, Date}}); {D1, T1} = {{Y, M, D}, {H, M1, S}} @@ -138,27 +138,45 @@ nparse(Date) -> {DateS, {H, M, S, Ms} } -> GSeconds = calendar:datetime_to_gregorian_seconds({DateS, {H, M, S} }), ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970, - {ESeconds div 1000000, ESeconds rem 1000000, Ms}; + {ESeconds div 1_000_000, ESeconds rem 1_000_000, Ms}; DateTime -> GSeconds = calendar:datetime_to_gregorian_seconds(DateTime), ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970, - {ESeconds div 1000000, ESeconds rem 1000000, 0} + {ESeconds div 1_000_000, ESeconds rem 1_000_000, 0} end. %% %% LOCAL FUNCTIONS %% +parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $Z ], _Now, _Opts) + when ?is_world_sep(X) + andalso (Micros >= 0 andalso Micros < 1_000_000) + andalso Year > 31 -> + {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Micros}}; + parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $Z ], _Now, _Opts) when (?is_us_sep(X) orelse ?is_world_sep(X)) andalso Year > 31 -> - {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, { 0}}; + {{Year, Month, Day}, {hour(Hour, []), Min, Sec}}; + +parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $+, Off | _Rest ], _Now, _Opts) + when (?is_us_sep(X) orelse ?is_world_sep(X)) + andalso (Micros >= 0 andalso Micros < 1_000_000) + andalso Year > 31 -> + {{Year, Month, Day}, {hour(Hour, []) - Off, Min, Sec}, {Micros}}; parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $+, Off | _Rest ], _Now, _Opts) when (?is_us_sep(X) orelse ?is_world_sep(X)) andalso Year > 31 -> {{Year, Month, Day}, {hour(Hour, []) - Off, Min, Sec}, {0}}; +parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $., Micros, $-, Off | _Rest ], _Now, _Opts) + when (?is_us_sep(X) orelse ?is_world_sep(X)) + andalso (Micros >= 0 andalso Micros < 1_000_000) + andalso Year > 31 -> + {{Year, Month, Day}, {hour(Hour, []) + Off, Min, Sec}, {Micros}}; + parse([Year, X, Month, X, Day, Hour, $:, Min, $:, Sec, $-, Off | _Rest ], _Now, _Opts) when (?is_us_sep(X) orelse ?is_world_sep(X)) andalso Year > 31 -> @@ -179,17 +197,6 @@ parse([Day,X,Month,X,Year,Hour,$:,Min,$:,Sec,$., Ms | PAM], _Now, _Opts) andalso ?is_year(Year) -> {{Year, Month, Day}, {hour(Hour, PAM), Min, Sec}, {Ms}}; -parse([Year,X,Month,X,Day,Hour,$:,Min,$:,Sec,$., Ms], _Now, _Opts) - when (?is_us_sep(X) orelse ?is_world_sep(X)) - andalso ?is_year(Year) -> - {{Year, Month, Day}, {hour(Hour,[]), Min, Sec}, {Ms}}; -parse([Month,X,Day,X,Year,Hour,$:,Min,$:,Sec,$., Ms], _Now, _Opts) - when ?is_us_sep(X) andalso ?is_month(Month) -> - {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Ms}}; -parse([Day,X,Month,X,Year,Hour,$:,Min,$:,Sec,$., Ms ], _Now, _Opts) - when ?is_world_sep(X) andalso ?is_month(Month) -> - {{Year, Month, Day}, {hour(Hour, []), Min, Sec}, {Ms}}; - %% Date/Times Dec 1st, 2012 6:25 PM parse([Month,Day,Year,Hour,$:,Min,$:,Sec | PAM], _Now, _Opts) when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) -> @@ -201,14 +208,6 @@ parse([Month,Day,Year,Hour | PAM], _Now, _Opts) when ?is_meridian(PAM) andalso ?is_hinted_month(Month) andalso ?is_day(Day) -> {{Year, Month, Day}, {hour(Hour, PAM), 0, 0}}; -%% Date/Times Dec 1st, 2012 18:25:15 (no AM/PM) -parse([Month,Day,Year,Hour,$:,Min,$:,Sec], _Now, _Opts) - when ?is_hinted_month(Month) andalso ?is_day(Day) -> - {{Year, Month, Day}, {hour(Hour, []), Min, Sec}}; -parse([Month,Day,Year,Hour,$:,Min], _Now, _Opts) - when ?is_hinted_month(Month) andalso ?is_day(Day) -> - {{Year, Month, Day}, {hour(Hour, []), Min, 0}}; - %% Date/Times Fri Nov 21 14:55:26 +0000 2014 (Twitter format) parse([Month, Day, Hour,$:,Min,$:,Sec, Year], _Now, _Opts) when ?is_hinted_month(Month), ?is_day(Day), ?is_year(Year) -> @@ -306,6 +305,23 @@ parse(_Tokens, _Now, _Opts) -> tokenise([], Acc) -> lists:reverse(Acc); +%% ISO 8601 fractions of a second +tokenise([$., N1, N2, N3, N4, N5, N6 | Rest], Acc) + when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5), ?is_num(N6) -> + tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5, N6]), $. | Acc]); +tokenise([$., N1, N2, N3, N4, N5 | Rest], Acc) + when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5) -> + tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5]) * 10, $. | Acc]); +tokenise([$., N1, N2, N3, N4 | Rest], Acc) + when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4) -> + tokenise(Rest, [ ltoi([N1, N2, N3, N4]) * 100, $. | Acc]); +tokenise([$., N1, N2, N3 | Rest], Acc) when ?is_num(N1), ?is_num(N2), ?is_num(N3) -> + tokenise(Rest, [ ltoi([N1, N2, N3]) * 1_000, $. | Acc]); +tokenise([$., N1, N2 | Rest], Acc) when ?is_num(N1), ?is_num(N2) -> + tokenise(Rest, [ ltoi([N1, N2]) * 10_000, $. | Acc]); +tokenise([$., N1 | Rest], Acc) when ?is_num(N1) -> + tokenise(Rest, [ ltoi([N1]) * 100_000, $. | Acc]); + tokenise([N1, N2, N3, N4, N5, N6 | Rest], Acc) when ?is_num(N1), ?is_num(N2), ?is_num(N3), ?is_num(N4), ?is_num(N5), ?is_num(N6) -> tokenise(Rest, [ ltoi([N1, N2, N3, N4, N5, N6]) | Acc]); @@ -405,7 +421,6 @@ tokenise("ST"++Rest, Acc) -> tokenise(Rest, Acc); tokenise("OF"++Rest, Acc) -> tokenise(Rest, Acc); tokenise("T"++Rest, Acc) -> tokenise(Rest, Acc); % 2012-12-12T12:12:12 ISO formatting. tokenise([$Z | Rest], Acc) -> tokenise(Rest, [$Z | Acc]); % 2012-12-12T12:12:12Zulu -tokenise([$. | Rest], Acc) -> tokenise(Rest, [$. | Acc]); % 2012-12-12T12:12:12.xxxx ISO formatting. tokenise([$+, H1,H2,M1,M2| Rest], Acc) when ?is_tz_offset(H1,H2,M1,M2) -> tokenise(Rest, Acc); % Tue Nov 11 15:03:18 +0000 2014 Twitter format tokenise([$+| Rest], Acc) -> tokenise(Rest, [$+ | Acc]); % 2012-12-12T12:12:12.xxxx+ ISO formatting. @@ -518,7 +533,7 @@ format([$g|T], {_,{H,_,_,_}}=Dt, Acc) when H > 12 -> format([$g|T], {_,{H,_,_,_}}=Dt, Acc) -> format(T, Dt, [itol(H)|Acc]); format([$G|T], {_,{H,_,_,_}}=Dt, Acc) -> - format(T, Dt, [itol(H)|Acc]); + format(T, Dt, [pad2(H)|Acc]); format([$h|T], {_,{H,_,_,_}}=Dt, Acc) when H > 12 -> format(T, Dt, [pad2(H-12)|Acc]); format([$h|T], {_,{H,_,_,_}}=Dt, Acc) -> @@ -530,7 +545,7 @@ format([$i|T], {_,{_,M,_,_}}=Dt, Acc) -> format([$s|T], {_,{_,_,S,_}}=Dt, Acc) -> format(T, Dt, [pad2(S)|Acc]); format([$f|T], {_,{_,_,_,Ms}}=Dt, Acc) -> - format(T, Dt, [itol(Ms)|Acc]); + format(T, Dt, [pad6(Ms)|Acc]); %% Whole Dates format([$c|T], {{Y,M,D},{H,Min,S}}=Dt, Acc) -> @@ -680,13 +695,17 @@ iso_week_one(Y) -> itol(X) -> integer_to_list(X). --spec pad2(integer()) -> list(). +-spec pad2(integer() | float()) -> list(). %% @doc int padded with 0 to make sure its 2 chars pad2(X) when is_integer(X) -> io_lib:format("~2.10.0B",[X]); pad2(X) when is_float(X) -> io_lib:format("~2.10.0B",[trunc(X)]). +-spec pad6(integer()) -> list(). +pad6(X) when is_integer(X) -> + io_lib:format("~6.10.0B",[X]). + ltoi(X) -> list_to_integer(X). @@ -694,12 +713,12 @@ ltoi(X) -> %%% Tests %%%=================================================================== --ifdef(DEV_ONLY). +-ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -define(DATE, {{2001,3,10},{17,16,17}}). --define(DATEMS, {{2001,3,10},{17,16,17,123456}}). +-define(DATEMS, {{2001,3,10},{17,16,17,123_456}}). -define(DATE_NOON, {{2001,3,10},{12,0,0}}). -define(DATE_MIDNIGHT, {{2001,3,10},{0,0,0}}). -define(ISO, "o \\WW"). @@ -718,6 +737,8 @@ basic_format_test_() -> ?_assertEqual(format("H:i:s",?DATE), "17:16:17"), ?_assertEqual(format("z",?DATE), "68"), ?_assertEqual(format("D M j G:i:s Y",?DATE), "Sat Mar 10 17:16:17 2001"), + ?_assertEqual(format("D M j G:i:s Y", {{2001,3,10},{5,16,17}}), "Sat Mar 10 5:16:17 2001"), + ?_assertEqual(format("D M j H:i:s Y", {{2001,3,10},{5,16,17}}), "Sat Mar 10 05:16:17 2001"), ?_assertEqual(format("ga",?DATE_NOON), "12pm"), ?_assertEqual(format("gA",?DATE_NOON), "12PM"), ?_assertEqual(format("ga",?DATE_MIDNIGHT), "12am"), @@ -933,7 +954,8 @@ iso_test_() -> ms_test_() -> Now=os:timestamp(), [ - ?_assertEqual({{2012,12,12}, {12,12,12,1234}}, parse("2012-12-12T12:12:12.1234")), + ?_assertEqual({{2012,12,12}, {12,12,12,1234}}, parse("2012-12-12T12:12:12.001234")), + ?_assertEqual({{2012,12,12}, {12,12,12,123_000}}, parse("2012-12-12T12:12:12.123")), ?_assertEqual(format("H:m:s.f \\m \\i\\s \\m\\o\\n\\t\\h",?DATEMS), "17:03:17.123456 m is month"), ?_assertEqual(format("Y-m-d\\TH:i:s.f",?DATEMS), @@ -944,6 +966,8 @@ ms_test_() -> "2001-03-10T05:16:17.123456"), ?_assertEqual(format("Y-m-d\\TH:i:s.f",nparse("2001-03-10T15:16:17.123456")), "2001-03-10T15:16:17.123456"), + ?_assertEqual(format("Y-m-d\\TH:i:s.f",nparse("2001-03-10T15:16:17.000123")), + "2001-03-10T15:16:17.000123"), ?_assertEqual(Now, nparse(format("Y-m-d\\TH:i:s.f", Now))) ]. @@ -965,8 +989,94 @@ zulu_test_() -> format_iso8601_test_() -> [ - ?_assertEqual("2001-03-10T17:16:17Z", format_iso8601(?DATE)), - ?_assertEqual("2001-03-10T17:16:17.123456Z", format_iso8601(?DATEMS)) + ?_assertEqual("2001-03-10T17:16:17Z", + format_iso8601({{2001,3,10},{17,16,17}})), + ?_assertEqual("2001-03-10T17:16:17.000000Z", + format_iso8601({{2001,3,10},{17,16,17,0}})), + ?_assertEqual("2001-03-10T17:16:17.100000Z", + format_iso8601({{2001,3,10},{17,16,17,100_000}})), + ?_assertEqual("2001-03-10T17:16:17.120000Z", + format_iso8601({{2001,3,10},{17,16,17,120_000}})), + ?_assertEqual("2001-03-10T17:16:17.123000Z", + format_iso8601({{2001,3,10},{17,16,17,123_000}})), + ?_assertEqual("2001-03-10T17:16:17.123400Z", + format_iso8601({{2001,3,10},{17,16,17,123_400}})), + ?_assertEqual("2001-03-10T17:16:17.123450Z", + format_iso8601({{2001,3,10},{17,16,17,123_450}})), + ?_assertEqual("2001-03-10T17:16:17.123456Z", + format_iso8601({{2001,3,10},{17,16,17,123_456}})), + ?_assertEqual("2001-03-10T17:16:17.023456Z", + format_iso8601({{2001,3,10},{17,16,17,23_456}})), + ?_assertEqual("2001-03-10T17:16:17.003456Z", + format_iso8601({{2001,3,10},{17,16,17,3_456}})), + ?_assertEqual("2001-03-10T17:16:17.000456Z", + format_iso8601({{2001,3,10},{17,16,17,456}})), + ?_assertEqual("2001-03-10T17:16:17.000056Z", + format_iso8601({{2001,3,10},{17,16,17,56}})), + ?_assertEqual("2001-03-10T17:16:17.000006Z", + format_iso8601({{2001,3,10},{17,16,17,6}})), + ?_assertEqual("2001-03-10T07:16:17Z", + format_iso8601({{2001,3,10},{07,16,17}})), + ?_assertEqual("2001-03-10T07:16:17.000000Z", + format_iso8601({{2001,3,10},{07,16,17,0}})), + ?_assertEqual("2001-03-10T07:16:17.100000Z", + format_iso8601({{2001,3,10},{07,16,17,100_000}})), + ?_assertEqual("2001-03-10T07:16:17.120000Z", + format_iso8601({{2001,3,10},{07,16,17,120_000}})), + ?_assertEqual("2001-03-10T07:16:17.123000Z", + format_iso8601({{2001,3,10},{07,16,17,123_000}})), + ?_assertEqual("2001-03-10T07:16:17.123400Z", + format_iso8601({{2001,3,10},{07,16,17,123_400}})), + ?_assertEqual("2001-03-10T07:16:17.123450Z", + format_iso8601({{2001,3,10},{07,16,17,123_450}})), + ?_assertEqual("2001-03-10T07:16:17.123456Z", + format_iso8601({{2001,3,10},{07,16,17,123_456}})), + ?_assertEqual("2001-03-10T07:16:17.023456Z", + format_iso8601({{2001,3,10},{07,16,17,23_456}})), + ?_assertEqual("2001-03-10T07:16:17.003456Z", + format_iso8601({{2001,3,10},{07,16,17,3_456}})), + ?_assertEqual("2001-03-10T07:16:17.000456Z", + format_iso8601({{2001,3,10},{07,16,17,456}})), + ?_assertEqual("2001-03-10T07:16:17.000056Z", + format_iso8601({{2001,3,10},{07,16,17,56}})), + ?_assertEqual("2001-03-10T07:16:17.000006Z", + format_iso8601({{2001,3,10},{07,16,17,6}})) + ]. + +parse_iso8601_test_() -> + [ + ?_assertEqual({{2001,3,10},{17,16,17}}, + parse("2001-03-10T17:16:17Z")), + ?_assertEqual({{2001,3,10},{17,16,17,0}}, + parse("2001-03-10T17:16:17.000Z")), + ?_assertEqual({{2001,3,10},{17,16,17,0}}, + parse("2001-03-10T17:16:17.000000Z")), + ?_assertEqual({{2001,3,10},{17,16,17,100_000}}, + parse("2001-03-10T17:16:17.1Z")), + ?_assertEqual({{2001,3,10},{17,16,17,120_000}}, + parse("2001-03-10T17:16:17.12Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123_000}}, + parse("2001-03-10T17:16:17.123Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123_400}}, + parse("2001-03-10T17:16:17.1234Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123_450}}, + parse("2001-03-10T17:16:17.12345Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123_456}}, + parse("2001-03-10T17:16:17.123456Z")), + + ?_assertEqual({{2001,3,10},{15,16,17,100_000}}, + parse("2001-03-10T16:16:17.1+01:00")), + ?_assertEqual({{2001,3,10},{15,16,17,123_456}}, + parse("2001-03-10T16:16:17.123456+01:00")), + ?_assertEqual({{2001,3,10},{17,16,17,100_000}}, + parse("2001-03-10T16:16:17.1-01:00")), + ?_assertEqual({{2001,3,10},{17,16,17,123_456}}, + parse("2001-03-10T16:16:17.123456-01:00")), + + ?_assertEqual({{2001,3,10},{17,16,17,456}}, + parse("2001-03-10T17:16:17.000456Z")), + ?_assertEqual({{2001,3,10},{17,16,17,123_000}}, + parse("2001-03-10T17:16:17.123000Z")) ]. -endif. diff --git a/src/ec_dict.erl b/src/ec_dict.erl index 0c0b998..3e9418e 100644 --- a/src/ec_dict.erl +++ b/src/ec_dict.erl @@ -34,11 +34,7 @@ %%%=================================================================== %% This should be opaque, but that kills dialyzer so for now we export it %% however you should not rely on the internal representation here --ifdef(namespaced_types). -type dictionary(_K, _V) :: dict:dict(). --else. --type dictionary(_K, _V) :: dict(). --endif. %%%=================================================================== %%% API diff --git a/src/ec_dictionary.erl b/src/ec_dictionary.erl index 423914a..ea7fdc9 100644 --- a/src/ec_dictionary.erl +++ b/src/ec_dictionary.erl @@ -42,8 +42,6 @@ -type key(T) :: T. -type value(T) :: T. --ifdef(have_callback_support). - -callback new() -> any(). -callback has_key(key(any()), any()) -> boolean(). -callback get(key(any()), any()) -> any(). @@ -55,27 +53,6 @@ -callback from_list([{key(any()), value(any())}]) -> any(). -callback keys(any()) -> [key(any())]. --else. - -%% In the case where R14 or lower is being used to compile the system -%% we need to export a behaviour info --export([behaviour_info/1]). --spec behaviour_info(atom()) -> [{atom(), arity()}] | undefined. -behaviour_info(callbacks) -> - [{new, 0}, - {has_key, 2}, - {get, 2}, - {add, 3}, - {remove, 2}, - {has_value, 2}, - {size, 1}, - {to_list, 1}, - {from_list, 1}, - {keys, 1}]; -behaviour_info(_Other) -> - undefined. --endif. - %%%=================================================================== %%% API %%%=================================================================== diff --git a/src/ec_file.erl b/src/ec_file.erl index e283c64..ddbee40 100644 --- a/src/ec_file.erl +++ b/src/ec_file.erl @@ -11,6 +11,7 @@ exists/1, copy/2, copy/3, + copy_file_info/3, insecure_mkdtemp/0, mkdir_path/1, mkdir_p/1, @@ -40,7 +41,8 @@ %%============================================================================ %% Types %%============================================================================ --type option() :: recursive. +-type file_info() :: mode | time | owner | group. +-type option() :: recursive | {file_info, [file_info()]}. %%%=================================================================== %%% API @@ -57,53 +59,100 @@ exists(Filename) -> %% @doc copy an entire directory to another location. -spec copy(file:name(), file:name(), Options::[option()]) -> ok | {error, Reason::term()}. copy(From, To, []) -> - copy(From, To); -copy(From, To, [recursive] = Options) -> - case is_dir(From) of - false -> - copy(From, To); + copy_(From, To, []); +copy(From, To, Options) -> + case proplists:get_value(recursive, Options, false) of true -> - make_dir_if_dir(To), - copy_subfiles(From, To, Options) + case is_dir(From) of + false -> + copy_(From, To, Options); + true -> + make_dir_if_dir(To), + copy_subfiles(From, To, Options) + end; + false -> + copy_(From, To, Options) end. %% @doc copy a file including timestamps,ownership and mode etc. -spec copy(From::file:filename(), To::file:filename()) -> ok | {error, Reason::term()}. copy(From, To) -> - case file:copy(From, To) of + copy_(From, To, [{file_info, [mode, time, owner, group]}]). + +copy_(From, To, Options) -> + Linked + = case file:read_link(From) of + {ok, Linked0} -> Linked0; + {error, _} -> undefined + end, + case Linked =/= undefined orelse file:copy(From, To) of + true -> + file:make_symlink(Linked, To); {ok, _} -> - case file:read_file_info(From) of - {ok, FileInfo} -> - case file:write_file_info(To, FileInfo) of - ok -> - ok; - {error, WFError} -> - {error, {write_file_info_failed, WFError}} - end; - {error, RFError} -> - {error, {read_file_info_failed, RFError}} - end; + copy_file_info(To, From, proplists:get_value(file_info, Options, [])); {error, Error} -> {error, {copy_failed, Error}} end. -%% @doc return an md5 checksum string or a binary. Same as unix utility of -%% same name. +copy_file_info(To, From, FileInfoToKeep) -> + case file:read_file_info(From) of + {ok, FileInfo} -> + case write_file_info(To, FileInfo, FileInfoToKeep) of + [] -> + ok; + Errors -> + {error, {write_file_info_failed_for, Errors}} + end; + {error, RFError} -> + {error, {read_file_info_failed, RFError}} + end. + +write_file_info(To, FileInfo, FileInfoToKeep) -> + WriteInfoFuns = [{mode, fun try_write_mode/2}, + {time, fun try_write_time/2}, + {group, fun try_write_group/2}, + {owner, fun try_write_owner/2}], + lists:foldl(fun(Info, Acc) -> + case proplists:get_value(Info, WriteInfoFuns, undefined) of + undefined -> + Acc; + F -> + case F(To, FileInfo) of + ok -> + Acc; + {error, Reason} -> + [{Info, Reason} | Acc] + end + end + end, [], FileInfoToKeep). + + +try_write_mode(To, #file_info{mode=Mode}) -> + file:write_file_info(To, #file_info{mode=Mode}). + +try_write_time(To, #file_info{atime=Atime, mtime=Mtime}) -> + file:write_file_info(To, #file_info{atime=Atime, mtime=Mtime}). + +try_write_owner(To, #file_info{uid=OwnerId}) -> + file:write_file_info(To, #file_info{uid=OwnerId}). + +try_write_group(To, #file_info{gid=OwnerId}) -> + file:write_file_info(To, #file_info{gid=OwnerId}). + +%% @doc return the MD5 digest of a string or a binary, +%% named after the UNIX utility. -spec md5sum(string() | binary()) -> string(). md5sum(Value) -> - hex(binary_to_list(erlang:md5(Value))). + bin_to_hex(crypto:hash(md5, Value)). -%% @doc return an sha1sum checksum string or a binary. Same as unix utility of -%% same name. --ifdef(deprecated_crypto). +%% @doc return the SHA-1 digest of a string or a binary, +%% named after the UNIX utility. -spec sha1sum(string() | binary()) -> string(). sha1sum(Value) -> - hex(binary_to_list(crypto:sha(Value))). --else. --spec sha1sum(string() | binary()) -> string(). -sha1sum(Value) -> - hex(binary_to_list(crypto:hash(sha, Value))). --endif. + bin_to_hex(crypto:hash(sha, Value)). + +bin_to_hex(Bin) -> + hex(binary_to_list(Bin)). %% @doc delete a file. Use the recursive option for directories. %%
@@ -122,7 +171,7 @@ remove(Path, Options) ->
 remove(Path) ->
     remove(Path, []).
 
-%% @doc indicates witha boolean if the path supplied refers to symlink.
+%% @doc indicates with a boolean if the path supplied refers to symlink.
 -spec is_symlink(file:name()) -> boolean().
 is_symlink(Path) ->
     case file:read_link_info(Path) of
@@ -172,10 +221,9 @@ real_dir_path(Path) ->
 
 %% @doc make a unique temporary directory. Similar function to BSD stdlib
 %% function of the same name.
--spec insecure_mkdtemp() -> TmpDirPath::file:name().
+-spec insecure_mkdtemp() -> TmpDirPath::file:name() | {error, term()}.
 insecure_mkdtemp() ->
-    random:seed(os:timestamp()),
-    UniqueNumber = erlang:integer_to_list(erlang:trunc(random:uniform() * 1000000000000)),
+    UniqueNumber = erlang:integer_to_list(erlang:trunc(rand:uniform() * 1_000_000_000_000)),
     TmpDirPath =
         filename:join([tmp(), lists:flatten([".tmp_dir", UniqueNumber])]),
 
@@ -201,7 +249,7 @@ mkdir_path(Path) ->
     mkdir_p(Path).
 
 
-%% @doc read a file from the file system. Provide UEX exeption on failure.
+%% @doc read a file from the file system. Provide UEX exception on failure.
 -spec read(FilePath::file:filename()) -> {ok, binary()} | {error, Reason::term()}.
 read(FilePath) ->
     %% Now that we are moving away from exceptions again this becomes
@@ -210,7 +258,7 @@ read(FilePath) ->
     file:read_file(FilePath).
 
 
-%% @doc write a file to the file system. Provide UEX exeption on failure.
+%% @doc write a file to the file system. Provide UEX exception on failure.
 -spec write(FileName::file:filename(), Contents::string()) -> ok | {error, Reason::term()}.
 write(FileName, Contents) ->
     %% Now that we are moving away from exceptions again this becomes
@@ -278,9 +326,15 @@ remove_recursive(Path, Options) ->
 tmp() ->
     case erlang:system_info(system_architecture) of
         "win32" ->
-            "./tmp";
+            case os:getenv("TEMP") of
+                false -> "./tmp";
+                Val -> Val
+            end;
         _SysArch ->
-            "/tmp"
+            case os:getenv("TMPDIR") of
+                false -> "/tmp";
+                Val -> Val
+            end
     end.
 
 %% Copy the subfiles of the From directory to the to directory.
@@ -321,91 +375,3 @@ hex0(I)  -> $0 + I.
 sub_files(From) ->
     {ok, SubFiles} = file:list_dir(From),
     [filename:join(From, SubFile) || SubFile <- SubFiles].
-%%%===================================================================
-%%% Test Functions
-%%%===================================================================
-
--ifdef(DEV_ONLY).
--include_lib("eunit/include/eunit.hrl").
-
-setup_test() ->
-    Dir = insecure_mkdtemp(),
-    mkdir_path(Dir),
-    ?assertMatch(false, is_symlink(Dir)),
-    ?assertMatch(true, filelib:is_dir(Dir)).
-
-md5sum_test() ->
-    ?assertMatch("cfcd208495d565ef66e7dff9f98764da", md5sum("0")).
-
-sha1sum_test() ->
-    ?assertMatch("b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", sha1sum("0")).
-
-file_test() ->
-    Dir = insecure_mkdtemp(),
-    TermFile = filename:join(Dir, "ec_file/dir/file.term"),
-    TermFileCopy = filename:join(Dir, "ec_file/dircopy/file.term"),
-    filelib:ensure_dir(TermFile),
-    filelib:ensure_dir(TermFileCopy),
-    write_term(TermFile, "term"),
-    ?assertMatch({ok, <<"\"term\". ">>}, read(TermFile)),
-    copy(filename:dirname(TermFile),
-         filename:dirname(TermFileCopy),
-         [recursive]).
-
-teardown_test() ->
-    Dir = insecure_mkdtemp(),
-    remove(Dir, [recursive]),
-    ?assertMatch(false, filelib:is_dir(Dir)).
-
-setup_base_and_target() ->
-    BaseDir = insecure_mkdtemp(),
-    DummyContents = <<"This should be deleted">>,
-    SourceDir = filename:join([BaseDir, "source"]),
-    ok = file:make_dir(SourceDir),
-    Name1 = filename:join([SourceDir, "fileone"]),
-    Name2 = filename:join([SourceDir, "filetwo"]),
-    Name3 = filename:join([SourceDir, "filethree"]),
-    NoName = filename:join([SourceDir, "noname"]),
-
-    ok = file:write_file(Name1, DummyContents),
-    ok = file:write_file(Name2, DummyContents),
-    ok = file:write_file(Name3, DummyContents),
-    ok = file:write_file(NoName, DummyContents),
-    {BaseDir, SourceDir, {Name1, Name2, Name3, NoName}}.
-
-exists_test() ->
-    BaseDir = insecure_mkdtemp(),
-    SourceDir = filename:join([BaseDir, "source1"]),
-    NoName = filename:join([SourceDir, "noname"]),
-    ok = file:make_dir(SourceDir),
-    Name1 = filename:join([SourceDir, "fileone"]),
-    ok = file:write_file(Name1, <<"Testn">>),
-    ?assertMatch(true, exists(Name1)),
-    ?assertMatch(false, exists(NoName)).
-
-real_path_test() ->
-    BaseDir = "foo",
-    Dir = filename:absname(filename:join(BaseDir, "source1")),
-    LinkDir = filename:join([BaseDir, "link"]),
-    ok = mkdir_p(Dir),
-    file:make_symlink(Dir, LinkDir),
-    ?assertEqual(Dir, real_dir_path(LinkDir)),
-    ?assertEqual(directory, type(Dir)),
-    ?assertEqual(symlink, type(LinkDir)),
-    TermFile = filename:join(BaseDir, "test_file"),
-    ok = write_term(TermFile, foo),
-    ?assertEqual(file, type(TermFile)),
-    ?assertEqual(true, is_symlink(LinkDir)),
-    ?assertEqual(false, is_symlink(Dir)).
-
-find_test() ->
-    %% Create a directory in /tmp for the test. Clean everything afterwards
-    {BaseDir, _SourceDir, {Name1, Name2, Name3, _NoName}} = setup_base_and_target(),
-    Result = find(BaseDir, "file[a-z]+\$"),
-    ?assertMatch(3, erlang:length(Result)),
-    ?assertEqual(true, lists:member(Name1, Result)),
-    ?assertEqual(true, lists:member(Name2, Result)),
-    ?assertEqual(true, lists:member(Name3, Result)),
-    remove(BaseDir, [recursive]).
-
--endif.
diff --git a/src/ec_gb_trees.erl b/src/ec_gb_trees.erl
index a87ab86..cde3f1b 100644
--- a/src/ec_gb_trees.erl
+++ b/src/ec_gb_trees.erl
@@ -135,79 +135,3 @@ from_list(List) when is_list(List) ->
 -spec keys(gb_trees:tree(K,_V)) -> [ec_dictionary:key(K)].
 keys(Data) ->
     gb_trees:keys(Data).
-
-%%%===================================================================
-%%% Tests
-%%%===================================================================
-
-
--ifdef(DEV_ONLY).
--include_lib("eunit/include/eunit.hrl").
-
-%% For me unit testing initially is about covering the obvious case. A
-%% check to make sure that what you expect the tested functionality to
-%% do, it actually does. As time goes on and people detect bugs you
-%% add tests for those specific problems to the unit test suit.
-%%
-%% However, when getting started you can only test your basic
-%% expectations. So here are the expectations I have for the add
-%% functionality.
-%%
-%% 1) I can put arbitrary terms into the dictionary as keys
-%% 2) I can put arbitrary terms into the dictionary as values
-%% 3) When I put a value in the dictionary by a key, I can retrieve
-%% that same value
-%% 4) When I put a different value in the dictionary by key it does
-%% not change other key value pairs.
-%% 5) When I update a value the new value in available by the new key
-%% 6) When a value does not exist a not found exception is created
-
-add_test() ->
-    Dict0 = ec_dictionary:new(ec_gb_trees),
-
-    Key1 = foo,
-    Key2 = [1, 3],
-    Key3 = {"super"},
-    Key4 = <<"fabulous">>,
-    Key5 = {"Sona", 2, <<"Zuper">>},
-
-    Value1 = Key5,
-    Value2 = Key4,
-    Value3 = Key2,
-    Value4 = Key3,
-    Value5 = Key1,
-
-    Dict01 = ec_dictionary:add(Key1, Value1, Dict0),
-    Dict02 = ec_dictionary:add(Key3, Value3,
-                               ec_dictionary:add(Key2, Value2,
-                                                 Dict01)),
-    Dict1 =
-        ec_dictionary:add(Key5, Value5,
-                          ec_dictionary:add(Key4, Value4,
-                                            Dict02)),
-
-    ?assertMatch(Value1, ec_dictionary:get(Key1, Dict1)),
-    ?assertMatch(Value2, ec_dictionary:get(Key2, Dict1)),
-    ?assertMatch(Value3, ec_dictionary:get(Key3, Dict1)),
-    ?assertMatch(Value4, ec_dictionary:get(Key4, Dict1)),
-    ?assertMatch(Value5, ec_dictionary:get(Key5, Dict1)),
-
-
-    Dict2 = ec_dictionary:add(Key3, Value5,
-                              ec_dictionary:add(Key2, Value4, Dict1)),
-
-
-    ?assertMatch(Value1, ec_dictionary:get(Key1, Dict2)),
-    ?assertMatch(Value4, ec_dictionary:get(Key2, Dict2)),
-    ?assertMatch(Value5, ec_dictionary:get(Key3, Dict2)),
-    ?assertMatch(Value4, ec_dictionary:get(Key4, Dict2)),
-    ?assertMatch(Value5, ec_dictionary:get(Key5, Dict2)),
-
-
-    ?assertThrow(not_found, ec_dictionary:get(should_blow_up, Dict2)),
-    ?assertThrow(not_found, ec_dictionary:get("This should blow up too",
-                                              Dict2)).
-
-
-
--endif.
diff --git a/src/ec_git_vsn.erl b/src/ec_git_vsn.erl
index cc51a92..e67d8e4 100644
--- a/src/ec_git_vsn.erl
+++ b/src/ec_git_vsn.erl
@@ -17,6 +17,13 @@
 -export([new/0,
          vsn/1]).
 
+-ifdef(TEST).
+-export([parse_tags/1,
+	 get_patch_count/1,
+	 collect_default_refcount/1
+	]).
+-endif.
+
 -export_type([t/0]).
 
 %%%===================================================================
@@ -24,7 +31,7 @@
 %%%===================================================================
 %% This should be opaque, but that kills dialyzer so for now we export it
 %% however you should not rely on the internal representation here
--type t() :: {string()}.
+-type t() :: {}.
 
 %%%===================================================================
 %%% API
@@ -32,9 +39,9 @@
 
 -spec new() -> t().
 new() ->
-    {"v"}.
+    {}.
 
--spec vsn(t()) -> {ok, string()} | {error, Reason::any()}.
+-spec vsn(t()|string()) -> {ok, string()} | {error, Reason::any()}.
 vsn(Data) ->
     {Vsn, RawRef, RawCount} = collect_default_refcount(Data),
     {ok, build_vsn_string(Vsn, RawRef, RawCount)}.
@@ -52,7 +59,7 @@ collect_default_refcount(Data) ->
     RawCount =
         case Tag of
             undefined ->
-                os:cmd("git rev-list HEAD | wc -l");
+                os:cmd("git rev-list --count HEAD");
             _ ->
                 get_patch_count(Tag)
         end,
@@ -61,12 +68,7 @@ collect_default_refcount(Data) ->
 build_vsn_string(Vsn, RawRef, RawCount) ->
     %% Cleanup the tag and the Ref information. Basically leading 'v's and
     %% whitespace needs to go away.
-    RefTag = case RawRef of
-                 undefined ->
-                     "";
-                 RawRef ->
-                     [".ref", re:replace(RawRef, "\\s", "", [global])]
-             end,
+    RefTag = [".ref", re:replace(RawRef, "\\s", "", [global])],
     Count = erlang:iolist_to_binary(re:replace(RawCount, "\\s", "", [global])),
 
     %% Create the valid [semver](http://semver.org) version from the tag
@@ -80,20 +82,26 @@ build_vsn_string(Vsn, RawRef, RawCount) ->
 
 get_patch_count(RawRef) ->
     Ref = re:replace(RawRef, "\\s", "", [global]),
-    Cmd = io_lib:format("git rev-list ~s..HEAD | wc -l",
+    Cmd = io_lib:format("git rev-list --count ~ts..HEAD",
                          [Ref]),
-    os:cmd(Cmd).
-
--spec parse_tags(t()) -> {string()|undefined, ec_semver:version_string()}.
-parse_tags({Prefix}) ->
-    first_valid_tag(os:cmd("git log --oneline --decorate  | fgrep \"tag: \" -1000"), Prefix).
-
--spec first_valid_tag(string(), string()) -> {string()|undefined, ec_semver:version_string()}.
-first_valid_tag(Line, Prefix) ->
-    RE = lists:flatten(io_lib:format("(\\(|\\s)tag:\\s(~s([^,\\)]+))", [Prefix])),
-    case re:run(Line, RE, [{capture, [2, 3], list}]) of
-        {match,[Tag, Vsn]} ->
-            {Tag, Vsn};
-        nomatch ->
-            {undefined, "0.0.0"}
+    case os:cmd(Cmd) of
+        "fatal: " ++ _ ->
+            0;
+        Count ->
+            Count
+    end.
+
+-spec parse_tags(t()|string()) -> {string()|undefined, ec_semver:version_string()}.
+parse_tags({}) ->
+    parse_tags("");
+parse_tags(Pattern) ->
+    Cmd = io_lib:format("git describe --abbrev=0 --tags --match \"~ts*\"", [Pattern]),
+    Tag = os:cmd(Cmd),
+    case Tag of
+        "fatal: " ++ _ ->
+            {undefined, ""};
+        _ ->
+            Vsn  = string:slice(Tag, string:length(Pattern)),
+            Vsn1 = string:trim(string:trim(Vsn, leading, "v"), trailing, "\n"),
+            {Tag, Vsn1}
     end.
diff --git a/src/ec_lists.erl b/src/ec_lists.erl
index fd8fa5f..fed76d0 100644
--- a/src/ec_lists.erl
+++ b/src/ec_lists.erl
@@ -52,7 +52,7 @@ find(_Fun, []) ->
     error.
 
 %% @doc Fetch a value from the list. If the function returns true the
-%% value is returend. If processing reaches the end of the list and
+%% value is returned. If processing reaches the end of the list and
 %% the function has never returned true an exception not_found is
 %% thrown.
 -spec fetch(fun(), list()) -> term().
@@ -63,184 +63,3 @@ fetch(Fun, List) when is_list(List), is_function(Fun) ->
         error ->
             throw(not_found)
     end.
-
-%%%===================================================================
-%%% Test Functions
-%%%===================================================================
-
--ifdef(DEV_ONLY).
--include_lib("eunit/include/eunit.hrl").
-
-find1_test() ->
-    TestData = [1, 2, 3, 4, 5, 6],
-    Result = find(fun(5) ->
-                          true;
-                     (_) ->
-                          false
-                  end,
-                  TestData),
-    ?assertMatch({ok, 5}, Result),
-
-    Result2 = find(fun(37) ->
-                           true;
-                      (_) ->
-                           false
-                   end,
-                   TestData),
-    ?assertMatch(error, Result2).
-
-find2_test() ->
-    TestData = ["one", "two", "three", "four", "five", "six"],
-    Result = find(fun("five") ->
-                          true;
-                     (_) ->
-                          false
-                  end,
-                  TestData),
-    ?assertMatch({ok, "five"}, Result),
-
-    Result2 = find(fun(super_duper) ->
-                           true;
-                      (_) ->
-                           false
-                   end,
-                   TestData),
-    ?assertMatch(error, Result2).
-
-
-
-find3_test() ->
-    TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
-                {"six", 6}],
-    Result = find(fun({"one", 1}) ->
-                          true;
-                     (_) ->
-                          false
-                  end,
-                  TestData),
-    ?assertMatch({ok, {"one", 1}}, Result),
-
-    Result2 = find(fun([fo, bar, baz]) ->
-                           true;
-                      ({"onehundred", 100}) ->
-                           true;
-                      (_) ->
-                           false
-                   end,
-                   TestData),
-    ?assertMatch(error, Result2).
-
-
-
-fetch1_test() ->
-    TestData = [1, 2, 3, 4, 5, 6],
-    Result = fetch(fun(5) ->
-                           true;
-                      (_) ->
-                           false
-                   end,
-                   TestData),
-    ?assertMatch(5, Result),
-
-    ?assertThrow(not_found,
-                 fetch(fun(37) ->
-                               true;
-                          (_) ->
-                               false
-                       end,
-                       TestData)).
-
-fetch2_test() ->
-    TestData = ["one", "two", "three", "four", "five", "six"],
-    Result = fetch(fun("five") ->
-                           true;
-                      (_) ->
-                           false
-                   end,
-                   TestData),
-    ?assertMatch("five", Result),
-
-    ?assertThrow(not_found,
-                 fetch(fun(super_duper) ->
-                               true;
-                          (_) ->
-                               false
-                       end,
-                       TestData)).
-
-fetch3_test() ->
-    TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
-                {"six", 6}],
-    Result = fetch(fun({"one", 1}) ->
-                           true;
-                      (_) ->
-                           false
-                   end,
-                   TestData),
-    ?assertMatch({"one", 1}, Result),
-
-    ?assertThrow(not_found,
-                 fetch(fun([fo, bar, baz]) ->
-                               true;
-                          ({"onehundred", 100}) ->
-                               true;
-                          (_) ->
-                               false
-                       end,
-                       TestData)).
-
-search1_test() ->
-    TestData = [1, 2, 3, 4, 5, 6],
-    Result = search(fun(5) ->
-                            {ok, 5};
-                       (_) ->
-                            not_found
-                    end,
-                    TestData),
-    ?assertMatch({ok, 5, 5}, Result),
-
-    Result2 = search(fun(37) ->
-                             {ok, 37};
-                        (_) ->
-                             not_found
-                     end,
-                     TestData),
-    ?assertMatch(not_found, Result2).
-
-search2_test() ->
-    TestData = [1, 2, 3, 4, 5, 6],
-    Result = search(fun(1) ->
-                            {ok, 10};
-                       (_) ->
-                            not_found
-                    end,
-                    TestData),
-    ?assertMatch({ok, 10, 1}, Result),
-
-    Result2 = search(fun(6) ->
-                             {ok, 37};
-                        (_) ->
-                             not_found
-                     end,
-                     TestData),
-    ?assertMatch({ok, 37, 6}, Result2).
-
-search3_test() ->
-    TestData = [1, 2, 3, 4, 5, 6],
-    Result = search(fun(10) ->
-                            {ok, 10};
-                       (_) ->
-                            not_found
-                    end,
-                    TestData),
-    ?assertMatch(not_found, Result),
-
-    Result2 = search(fun(-1) ->
-                             {ok, 37};
-                        (_) ->
-                             not_found
-                     end,
-                     TestData),
-    ?assertMatch(not_found, Result2).
-
--endif.
diff --git a/src/ec_plists.erl b/src/ec_plists.erl
index 4726c18..221075b 100644
--- a/src/ec_plists.erl
+++ b/src/ec_plists.erl
@@ -30,7 +30,7 @@
 %%% most list operations parallel. It can operate on each element in
 %%% parallel, for IO-bound operations, on sublists in parallel, for
 %%% taking advantage of multi-core machines with CPU-bound operations,
-%%% and across erlang nodes, for parallizing inside a cluster. It
+%%% and across erlang nodes, for parallelizing inside a cluster. It
 %%% handles errors and node failures. It can be configured, tuned, and
 %%% tweaked to get optimal performance while minimizing overhead.
 %%%
@@ -38,7 +38,7 @@
 %%% lists, returning exactly the same result, and having both a form
 %%% with an identical syntax that operates on each element in parallel
 %%% and a form which takes an optional "malt", a specification for how
-%%% to parallize the operation.
+%%% to parallelize the operation.
 %%%
 %%% fold is the one exception, parallel fold is different from linear
 %%% fold.  This module also include a simple mapreduce implementation,
@@ -169,7 +169,7 @@
 %%% processes. If one of them does a non-normal exit, plists receives
 %%% the 'DOWN' message believing it to be from one of its own
 %%% processes. The error propagation system goes into effect, which
-%%% results in the error occuring in the calling process.
+%%% results in the error occurring in the calling process.
 %%%
 -module(ec_plists).
 
@@ -217,13 +217,13 @@
 
 %% @doc Same semantics as in module
 %% lists.
--spec all/2 :: (el_fun(), list()) -> boolean().
+-spec all(el_fun(), list()) -> boolean().
 all(Fun, List) ->
     all(Fun, List, 1).
 
 %% @doc Same semantics as in module
 %% lists.
--spec all/3 :: (el_fun(), list(), malt()) -> boolean().
+-spec all(el_fun(), list(), malt()) -> boolean().
 all(Fun, List, Malt) ->
     try
         runmany(fun (L) ->
@@ -247,13 +247,13 @@ all(Fun, List, Malt) ->
 
 %% @doc Same semantics as in module
 %% lists.
--spec any/2 :: (fun(), list()) -> boolean().
+-spec any(fun(), list()) -> boolean().
 any(Fun, List) ->
     any(Fun, List, 1).
 
 %% @doc Same semantics as in module
 %% lists.
--spec any/3 :: (fun(), list(), malt()) -> boolean().
+-spec any(fun(), list(), malt()) -> boolean().
 any(Fun, List, Malt) ->
     try
         runmany(fun (L) ->
@@ -276,13 +276,13 @@ any(Fun, List, Malt) ->
 
 %% @doc Same semantics as in module
 %% lists.
--spec filter/2 :: (fun(), list()) -> list().
+-spec filter(fun(), list()) -> list().
 filter(Fun, List) ->
     filter(Fun, List, 1).
 
 %% @doc Same semantics as in module
 %% lists.
--spec filter/3 :: (fun(), list(), malt()) -> list().
+-spec filter(fun(), list(), malt()) -> list().
 filter(Fun, List, Malt) ->
     runmany(fun (L) ->
                     lists:filter(Fun, L)
@@ -297,12 +297,12 @@ filter(Fun, List, Malt) ->
 
 %% @doc Like below, but assumes 1 as the Malt. This function is almost useless,
 %% and is intended only to aid converting code from using lists to plists.
--spec fold/3 :: (fun(), InitAcc::term(), list()) -> term().
+-spec fold(fun(), InitAcc::term(), list()) -> term().
 fold(Fun, InitAcc, List) ->
     fold(Fun, Fun, InitAcc, List, 1).
 
 %% @doc Like below, but uses the Fun as the Fuse by default.
--spec fold/4 :: (fun(), InitAcc::term(), list(), malt()) -> term().
+-spec fold(fun(), InitAcc::term(), list(), malt()) -> term().
 fold(Fun, InitAcc, List, Malt) ->
     fold(Fun, Fun, InitAcc, List, Malt).
 
@@ -323,24 +323,24 @@ fold(Fun, InitAcc, List, Malt) ->
 %%
 %% Malt is the malt for the initial folding of sublists, and for the
 %% possible recursive fuse.
--spec fold/5 :: (fun(), fuse(), InitAcc::term(), list(), malt()) -> term().
+-spec fold(fun(), fuse(), InitAcc::term(), list(), malt()) -> term().
 fold(Fun, Fuse, InitAcc, List, Malt) ->
     Fun2 = fun (L) ->
                    lists:foldl(Fun, InitAcc, L)
            end,
     runmany(Fun2, Fuse, List, Malt).
 
-%% @doc Similiar to foreach in module
+%% @doc Similar to foreach in module
 %% lists
 %% except it makes no guarantee about the order it processes list elements.
--spec foreach/2 :: (fun(), list()) -> ok.
+-spec foreach(fun(), list()) -> ok.
 foreach(Fun, List) ->
     foreach(Fun, List, 1).
 
-%% @doc Similiar to foreach in module
+%% @doc Similar to foreach in module
 %% lists
 %% except it makes no guarantee about the order it processes list elements.
--spec foreach/3 :: (fun(), list(), malt()) -> ok.
+-spec foreach(fun(), list(), malt()) -> ok.
 foreach(Fun, List, Malt) ->
     runmany(fun (L) ->
                     lists:foreach(Fun, L)
@@ -352,13 +352,13 @@ foreach(Fun, List, Malt) ->
 
 %% @doc Same semantics as in module
 %% lists.
--spec map/2 :: (fun(), list()) -> list().
+-spec map(fun(), list()) -> list().
 map(Fun, List) ->
     map(Fun, List, 1).
 
 %% @doc Same semantics as in module
 %% lists.
--spec map/3 :: (fun(), list(), malt()) -> list().
+-spec map(fun(), list(), malt()) -> list().
 map(Fun, List, Malt) ->
     runmany(fun (L) ->
                     lists:map(Fun, L)
@@ -369,7 +369,7 @@ map(Fun, List, Malt) ->
             List, Malt).
 
 %% @doc values are returned as {value, term()}.
--spec ftmap/2 :: (fun(), list()) -> list().
+-spec ftmap(fun(), list()) -> list().
 ftmap(Fun, List) ->
    map(fun(L) ->
                try
@@ -381,7 +381,7 @@ ftmap(Fun, List) ->
        end, List).
 
 %% @doc values are returned as {value, term()}.
--spec ftmap/3 :: (fun(), list(), malt()) -> list().
+-spec ftmap(fun(), list(), malt()) -> list().
 ftmap(Fun, List, Malt) ->
     map(fun(L) ->
                 try
@@ -394,13 +394,13 @@ ftmap(Fun, List, Malt) ->
 
 %% @doc Same semantics as in module
 %% lists.
--spec partition/2 :: (fun(), list()) -> {list(), list()}.
+-spec partition(fun(), list()) -> {list(), list()}.
 partition(Fun, List) ->
     partition(Fun, List, 1).
 
 %% @doc Same semantics as in module
 %% lists.
--spec partition/3 :: (fun(), list(), malt()) -> {list(), list()}.
+-spec partition(fun(), list(), malt()) -> {list(), list()}.
 partition(Fun, List, Malt) ->
     runmany(fun (L) ->
                     lists:partition(Fun, L)
@@ -415,7 +415,7 @@ partition(Fun, List, Malt) ->
 
 %% @doc Same semantics as in module
 %% lists.
--spec sort/1 :: (list()) -> list().
+-spec sort(list()) -> list().
 sort(List) ->
     sort(fun (A, B) ->
                  A =< B
@@ -424,7 +424,7 @@ sort(List) ->
 
 %% @doc Same semantics as in module
 %% lists.
--spec sort/2 :: (fun(), list()) -> list().
+-spec sort(fun(), list()) -> list().
 sort(Fun, List) ->
     sort(Fun, List, ?SORTMALT).
 
@@ -432,10 +432,10 @@ sort(Fun, List) ->
 %%
 %% sort splits the list into sublists and sorts them, and it merges the
 %% sorted lists together. These are done in parallel. Each sublist is
-%% sorted in a seperate process, and each merging of results is done in a
-%% seperate process. Malt defaults to 100, causing the list to be split into
+%% sorted in a separate process, and each merging of results is done in a
+%% separate process. Malt defaults to 100, causing the list to be split into
 %% 100-element sublists.
--spec sort/3 :: (fun(), list(), malt()) -> list().
+-spec sort(fun(), list(), malt()) -> list().
 sort(Fun, List, Malt) ->
     Fun2 = fun (L) ->
                    lists:sort(Fun, L)
@@ -447,7 +447,7 @@ sort(Fun, List, Malt) ->
 
 %% @doc Same semantics as in module
 %% lists.
--spec usort/1 :: (list()) -> list().
+-spec usort(list()) -> list().
 usort(List) ->
     usort(fun (A, B) ->
                   A =< B
@@ -456,7 +456,7 @@ usort(List) ->
 
 %% @doc Same semantics as in module
 %% lists.
--spec usort/2 :: (fun(), list()) -> list().
+-spec usort(fun(), list()) -> list().
 usort(Fun, List) ->
     usort(Fun, List, ?SORTMALT).
 
@@ -464,12 +464,12 @@ usort(Fun, List) ->
 %%
 %% usort splits the list into sublists and sorts them, and it merges the
 %% sorted lists together. These are done in parallel. Each sublist is
-%% sorted in a seperate process, and each merging of results is done in a
-%% seperate process. Malt defaults to 100, causing the list to be split into
+%% sorted in a separate process, and each merging of results is done in a
+%% separate process. Malt defaults to 100, causing the list to be split into
 %% 100-element sublists.
 %%
-%% usort removes duplicate elments while it sorts.
--spec usort/3 :: (fun(), list(), malt()) -> list().
+%% usort removes duplicate elements while it sorts.
+-spec usort(fun(), list(), malt()) -> list().
 usort(Fun, List, Malt) ->
     Fun2 = fun (L) ->
                    lists:usort(Fun, L)
@@ -480,16 +480,9 @@ usort(Fun, List, Malt) ->
     runmany(Fun2, {recursive, Fuse}, List, Malt).
 
 %% @doc Like below, assumes default MapMalt of 1.
--ifdef(namespaced_types).
--spec mapreduce/2 :: (MapFunc, list()) -> dict:dict() when
+-spec mapreduce(MapFunc, list()) -> dict:dict() when
       MapFunc ::  fun((term()) -> DeepListOfKeyValuePairs),
       DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}.
--else.
--spec mapreduce/2 :: (MapFunc, list()) -> dict() when
-      MapFunc ::  fun((term()) -> DeepListOfKeyValuePairs),
-      DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}.
--endif.
-
 
 mapreduce(MapFunc, List) ->
     mapreduce(MapFunc, List, 1).
@@ -514,21 +507,14 @@ mapreduce(MapFunc, List, MapMalt) ->
 %% reducer's final state.
 %%
 %% MapMalt is the malt for the mapping operation, with a default value of 1,
-%% meaning each element of the list is mapped by a seperate process.
+%% meaning each element of the list is mapped by a separate process.
 %%
 %% mapreduce requires OTP R11B, or it may leave monitoring messages in the
 %% message queue.
--ifdef(namespaced_types).
--spec mapreduce/5 :: (MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict:dict() when
+-spec mapreduce(MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict:dict() when
       MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
       DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()},
       ReduceFunc :: fun((OldState::term(), Key::term(), Value::term()) -> NewState::term()).
--else.
--spec mapreduce/5 :: (MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict() when
-      MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
-      DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()},
-      ReduceFunc :: fun((OldState::term(), Key::term(), Value::term()) -> NewState::term()).
--endif.
 mapreduce(MapFunc, List, InitState, ReduceFunc, MapMalt) ->
     Parent = self(),
     {Reducer, ReducerRef} =
@@ -586,8 +572,8 @@ add_key(Dict, Key, Value) ->
     end.
 
 %% @doc Like below, but assumes a Malt of 1,
-%% meaning each element of the list is processed by a seperate process.
--spec runmany/3 :: (fun(), fuse(), list()) -> term().
+%% meaning each element of the list is processed by a separate process.
+-spec runmany(fun(), fuse(), list()) -> term().
 runmany(Fun, Fuse, List) ->
     runmany(Fun, Fuse, List, 1).
 
@@ -615,14 +601,14 @@ runmany(Fun, Fuse, List) ->
 %% continues fusing pairs of results until it is down to one.
 %%
 %% Recursive fuse is down in parallel with processing the sublists, and a
-%% process is spawned to fuse each pair of results. It is a parallized
+%% process is spawned to fuse each pair of results. It is a parallelized
 %% algorithm. Linear fuse is done after all results of processing sublists
 %% have been collected, and can only run in a single process.
 %%
 %% Even if you pass {recursive, FuseFunc}, a recursive fuse is only done if
 %% the malt contains {nodes, NodeList} or {processes, X}. If this is not the
 %% case, a linear fuse is done.
--spec runmany/4 :: (fun(([term()]) -> term()), fuse(), list(), malt()) -> term().
+-spec runmany(fun(([term()]) -> term()), fuse(), list(), malt()) -> term().
 runmany(Fun, Fuse, List, Malt)
   when erlang:is_list(Malt) ->
     runmany(Fun, Fuse, List, local, no_split, Malt);
@@ -691,7 +677,7 @@ runmany(Fun, {recursive, Fuse}, List, local, Split, []) ->
     %% or {nodes, NodeList}. Degenerates recursive fuse into linear fuse.
     runmany(Fun, Fuse, List, local, Split, []);
 runmany(Fun, Fuse, List, Nodes, no_split, []) ->
-    %% by default, operate on each element seperately
+    %% by default, operate on each element separately
     runmany(Fun, Fuse, List, Nodes, 1, []);
 runmany(Fun, Fuse, List, local, Split, []) ->
     List2 = splitmany(List, Split),
@@ -772,8 +758,8 @@ receivefrom(Pid) ->
     receive
         {Pid, R} ->
             R;
-        {'DOWN', _, _, BadPid, Reason} when Reason =/= normal ->
-            erlang:throw({BadPid, Reason});
+        {'DOWN', _, _, Pid, Reason} when Reason =/= normal ->
+            erlang:throw({Pid, Reason});
         {timerrang, _} ->
             erlang:throw({nil, timeout})
     end.
@@ -822,20 +808,7 @@ cluster_runmany(Fun, Fuse, [Task|TaskList], [N|Nodes], Running, Results) ->
                            Parent ! {erlang:self(), fuse, FuseFunc(R1, R2)}
                    end
     end,
-    Fun3 = fun () ->
-                   try
-                       Fun2()
-                   catch
-                       exit:siblingdied ->
-                           ok;
-                       exit:Reason ->
-                           Parent ! {erlang:self(), error, Reason};
-                       error:R ->
-                           Parent ! {erlang:self(), error, {R, erlang:get_stacktrace()}};
-                       throw:R ->
-                           Parent ! {erlang:self(), error, {{nocatch, R}, erlang:get_stacktrace()}}
-                   end
-           end,
+    Fun3 = fun() -> runmany_wrap(Fun2, Parent) end,
     Pid = proc_lib:spawn(N, Fun3),
     erlang:monitor(process, Pid),
     cluster_runmany(Fun, Fuse, TaskList, Nodes, [{Pid, N, Task}|Running], Results);
@@ -885,6 +858,20 @@ cluster_runmany(_, _, [_Non|_Empty], []=_Nodes, []=_Running, _) ->
 %% We have data, but no nodes either available or occupied
     erlang:exit(allnodescrashed).
 
+runmany_wrap(Fun, Parent) ->
+    try
+        Fun()
+    catch
+        exit:siblingdied ->
+            ok;
+        exit:Reason ->
+            Parent ! {erlang:self(), error, Reason};
+        error:R:Stacktrace ->
+            Parent ! {erlang:self(), error, {R, Stacktrace}};
+        throw:R:Stacktrace ->
+            Parent ! {erlang:self(), error, {{nocatch, R}, Stacktrace}}
+    end.
+
 delete_running(Pid, [{Pid, Node, List}|Running], Acc) ->
     {Running ++ Acc, Node, List};
 delete_running(Pid, [R|Running], Acc) ->
diff --git a/src/ec_rbdict.erl b/src/ec_rbdict.erl
index 60e337f..9f3b617 100644
--- a/src/ec_rbdict.erl
+++ b/src/ec_rbdict.erl
@@ -32,7 +32,7 @@
 %%% representation of a dictionary, where a red-black tree is used to
 %%% store the keys and values.
 %%%
-%%% This module implents exactly the same interface as the module
+%%% This module implements exactly the same interface as the module
 %%% ec_dictionary but with a defined representation. One difference is
 %%% that while dict considers two keys as different if they do not
 %%% match (=:=), this module considers two keys as different if and
@@ -296,7 +296,7 @@ to_list(empty, List) -> List;
 to_list({_, A, Xk, Xv, B}, List) ->
     to_list(A, [{Xk, Xv} | to_list(B, List)]).
 
-%% Balance a tree afer (possibly) adding a node to the left/right.
+%% Balance a tree after (possibly) adding a node to the left/right.
 -spec lbalance(color(), dictionary(K, V),
                ec_dictionary:key(K), ec_dictionary:value(V),
                dictionary(K, V)) ->
diff --git a/src/ec_semver.erl b/src/ec_semver.erl
index 23ba311..3ffd591 100644
--- a/src/ec_semver.erl
+++ b/src/ec_semver.erl
@@ -202,13 +202,13 @@ pes(VsnA, VsnB) ->
 %%%===================================================================
 %%% Friend Functions
 %%%===================================================================
-%% @doc helper function for the peg grammer to parse the iolist into a semver
+%% @doc helper function for the peg grammar to parse the iolist into a semver
 -spec internal_parse_version(iolist()) -> semver().
 internal_parse_version([MMP, AlphaPart, BuildPart, _]) ->
     {parse_major_minor_patch_minpatch(MMP), {parse_alpha_part(AlphaPart),
                                              parse_alpha_part(BuildPart)}}.
 
-%% @doc helper function for the peg grammer to parse the iolist into a major_minor_patch
+%% @doc helper function for the peg grammar to parse the iolist into a major_minor_patch
 -spec parse_major_minor_patch_minpatch(iolist()) -> major_minor_patch_minpatch().
 parse_major_minor_patch_minpatch([MajVsn, [], [], []]) ->
     strip_maj_version(MajVsn);
@@ -224,7 +224,7 @@ parse_major_minor_patch_minpatch([MajVsn,
                                   [<<".">>, MinPatch]]) ->
     {strip_maj_version(MajVsn), MinVsn, PatchVsn, MinPatch}.
 
-%% @doc helper function for the peg grammer to parse the iolist into an alpha part
+%% @doc helper function for the peg grammar to parse the iolist into an alpha part
 -spec parse_alpha_part(iolist()) -> [alpha_part()].
 parse_alpha_part([]) ->
     [];
@@ -287,425 +287,25 @@ normalize(Other = {{_, _, _, _}, {_,_}}) ->
 %% the internal implementation of the of the pessimistic run. The
 %% external just ensures that versions are parsed.
 -spec internal_pes(semver(), semver()) -> boolean().
-internal_pes(VsnA, {{LM, LMI}, _})
+internal_pes(VsnA, {{LM, LMI}, Alpha})
   when erlang:is_integer(LM),
        erlang:is_integer(LMI) ->
-    gte(VsnA, {{LM, LMI, 0}, {[], []}}) andalso
+    gte(VsnA, {{LM, LMI, 0}, Alpha}) andalso
         lt(VsnA, {{LM + 1, 0, 0, 0}, {[], []}});
-internal_pes(VsnA, {{LM, LMI, LP}, _})
+internal_pes(VsnA, {{LM, LMI, LP}, Alpha})
     when erlang:is_integer(LM),
          erlang:is_integer(LMI),
          erlang:is_integer(LP) ->
-    gte(VsnA, {{LM, LMI, LP}, {[], []}})
+    gte(VsnA, {{LM, LMI, LP}, Alpha})
         andalso
         lt(VsnA, {{LM, LMI + 1, 0, 0}, {[], []}});
-internal_pes(VsnA, {{LM, LMI, LP, LMP}, _})
+internal_pes(VsnA, {{LM, LMI, LP, LMP}, Alpha})
     when erlang:is_integer(LM),
          erlang:is_integer(LMI),
          erlang:is_integer(LP),
          erlang:is_integer(LMP) ->
-    gte(VsnA, {{LM, LMI, LP, LMP}, {[], []}})
+    gte(VsnA, {{LM, LMI, LP, LMP}, Alpha})
         andalso
         lt(VsnA, {{LM, LMI, LP + 1, 0}, {[], []}});
 internal_pes(Vsn, LVsn) ->
     gte(Vsn, LVsn).
-
-%%%===================================================================
-%%% Test Functions
-%%%===================================================================
-
--ifdef(DEV_ONLY).
--include_lib("eunit/include/eunit.hrl").
-
-eql_test() ->
-    ?assertMatch(true, eql("1.0.0-alpha",
-                           "1.0.0-alpha")),
-    ?assertMatch(true, eql("v1.0.0-alpha",
-                           "1.0.0-alpha")),
-    ?assertMatch(true, eql("1",
-                           "1.0.0")),
-    ?assertMatch(true, eql("v1",
-                           "v1.0.0")),
-    ?assertMatch(true, eql("1.0",
-                           "1.0.0")),
-    ?assertMatch(true, eql("1.0.0",
-                           "1")),
-    ?assertMatch(true, eql("1.0.0.0",
-                           "1")),
-    ?assertMatch(true, eql("1.0+alpha.1",
-                           "1.0.0+alpha.1")),
-    ?assertMatch(true, eql("1.0-alpha.1+build.1",
-                           "1.0.0-alpha.1+build.1")),
-    ?assertMatch(true, eql("1.0-alpha.1+build.1",
-                           "1.0.0.0-alpha.1+build.1")),
-    ?assertMatch(true, eql("1.0-alpha.1+build.1",
-                           "v1.0.0.0-alpha.1+build.1")),
-    ?assertMatch(true, eql("aa", "aa")),
-    ?assertMatch(true, eql("AA.BB", "AA.BB")),
-    ?assertMatch(true, eql("BBB-super", "BBB-super")),
-    ?assertMatch(true, not eql("1.0.0",
-                               "1.0.1")),
-    ?assertMatch(true, not eql("1.0.0-alpha",
-                               "1.0.1+alpha")),
-    ?assertMatch(true, not eql("1.0.0+build.1",
-                               "1.0.1+build.2")),
-    ?assertMatch(true, not eql("1.0.0.0+build.1",
-                               "1.0.0.1+build.2")),
-    ?assertMatch(true, not eql("FFF", "BBB")),
-    ?assertMatch(true, not eql("1", "1BBBB")).
-
-gt_test() ->
-    ?assertMatch(true, gt("1.0.0-alpha.1",
-                          "1.0.0-alpha")),
-    ?assertMatch(true, gt("1.0.0.1-alpha.1",
-                          "1.0.0.1-alpha")),
-    ?assertMatch(true, gt("1.0.0.4-alpha.1",
-                          "1.0.0.2-alpha")),
-    ?assertMatch(true, gt("1.0.0.0-alpha.1",
-                          "1.0.0-alpha")),
-    ?assertMatch(true, gt("1.0.0-beta.2",
-                          "1.0.0-alpha.1")),
-    ?assertMatch(true, gt("1.0.0-beta.11",
-                          "1.0.0-beta.2")),
-    ?assertMatch(true, gt("1.0.0-beta.11",
-                          "1.0.0.0-beta.2")),
-    ?assertMatch(true, gt("1.0.0-rc.1", "1.0.0-beta.11")),
-    ?assertMatch(true, gt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
-    ?assertMatch(true, gt("1.0.0", "1.0.0-rc.1+build.1")),
-    ?assertMatch(true, gt("1.0.0+0.3.7", "1.0.0")),
-    ?assertMatch(true, gt("1.3.7+build", "1.0.0+0.3.7")),
-    ?assertMatch(true, gt("1.3.7+build.2.b8f12d7",
-                          "1.3.7+build")),
-    ?assertMatch(true, gt("1.3.7+build.2.b8f12d7",
-                          "1.3.7.0+build")),
-    ?assertMatch(true, gt("1.3.7+build.11.e0f985a",
-                          "1.3.7+build.2.b8f12d7")),
-    ?assertMatch(true, gt("aa.cc",
-                          "aa.bb")),
-    ?assertMatch(true, not gt("1.0.0-alpha",
-                              "1.0.0-alpha.1")),
-    ?assertMatch(true, not gt("1.0.0-alpha",
-                              "1.0.0.0-alpha.1")),
-    ?assertMatch(true, not gt("1.0.0-alpha.1",
-                              "1.0.0-beta.2")),
-    ?assertMatch(true, not gt("1.0.0-beta.2",
-                              "1.0.0-beta.11")),
-    ?assertMatch(true, not gt("1.0.0-beta.11",
-                              "1.0.0-rc.1")),
-    ?assertMatch(true, not gt("1.0.0-rc.1",
-                              "1.0.0-rc.1+build.1")),
-    ?assertMatch(true, not gt("1.0.0-rc.1+build.1",
-                              "1.0.0")),
-    ?assertMatch(true, not gt("1.0.0",
-                              "1.0.0+0.3.7")),
-    ?assertMatch(true, not gt("1.0.0+0.3.7",
-                              "1.3.7+build")),
-    ?assertMatch(true, not gt("1.3.7+build",
-                              "1.3.7+build.2.b8f12d7")),
-    ?assertMatch(true, not gt("1.3.7+build.2.b8f12d7",
-                              "1.3.7+build.11.e0f985a")),
-    ?assertMatch(true, not gt("1.0.0-alpha",
-                              "1.0.0-alpha")),
-    ?assertMatch(true, not gt("1",
-                              "1.0.0")),
-    ?assertMatch(true, not gt("aa.bb",
-                              "aa.bb")),
-    ?assertMatch(true, not gt("aa.cc",
-                              "aa.dd")),
-    ?assertMatch(true, not gt("1.0",
-                              "1.0.0")),
-    ?assertMatch(true, not gt("1.0.0",
-                              "1")),
-    ?assertMatch(true, not gt("1.0+alpha.1",
-                              "1.0.0+alpha.1")),
-    ?assertMatch(true, not gt("1.0-alpha.1+build.1",
-                              "1.0.0-alpha.1+build.1")).
-
-lt_test() ->
-    ?assertMatch(true, lt("1.0.0-alpha",
-                          "1.0.0-alpha.1")),
-    ?assertMatch(true, lt("1.0.0-alpha",
-                          "1.0.0.0-alpha.1")),
-    ?assertMatch(true, lt("1.0.0-alpha.1",
-                          "1.0.0-beta.2")),
-    ?assertMatch(true, lt("1.0.0-beta.2",
-                          "1.0.0-beta.11")),
-    ?assertMatch(true, lt("1.0.0-beta.11",
-                          "1.0.0-rc.1")),
-    ?assertMatch(true, lt("1.0.0.1-beta.11",
-                          "1.0.0.1-rc.1")),
-    ?assertMatch(true, lt("1.0.0-rc.1",
-                          "1.0.0-rc.1+build.1")),
-    ?assertMatch(true, lt("1.0.0-rc.1+build.1",
-                          "1.0.0")),
-    ?assertMatch(true, lt("1.0.0",
-                          "1.0.0+0.3.7")),
-    ?assertMatch(true, lt("1.0.0+0.3.7",
-                          "1.3.7+build")),
-    ?assertMatch(true, lt("1.3.7+build",
-                          "1.3.7+build.2.b8f12d7")),
-    ?assertMatch(true, lt("1.3.7+build.2.b8f12d7",
-                          "1.3.7+build.11.e0f985a")),
-    ?assertMatch(true, not lt("1.0.0-alpha",
-                              "1.0.0-alpha")),
-    ?assertMatch(true, not lt("1",
-                              "1.0.0")),
-    ?assertMatch(true, lt("1",
-                          "1.0.0.1")),
-    ?assertMatch(true, lt("AA.DD",
-                          "AA.EE")),
-    ?assertMatch(true, not lt("1.0",
-                              "1.0.0")),
-    ?assertMatch(true, not lt("1.0.0.0",
-                              "1")),
-    ?assertMatch(true, not lt("1.0+alpha.1",
-                              "1.0.0+alpha.1")),
-    ?assertMatch(true, not lt("AA.DD", "AA.CC")),
-    ?assertMatch(true, not lt("1.0-alpha.1+build.1",
-                              "1.0.0-alpha.1+build.1")),
-    ?assertMatch(true, not lt("1.0.0-alpha.1",
-                              "1.0.0-alpha")),
-    ?assertMatch(true, not lt("1.0.0-beta.2",
-                              "1.0.0-alpha.1")),
-    ?assertMatch(true, not lt("1.0.0-beta.11",
-                              "1.0.0-beta.2")),
-    ?assertMatch(true, not lt("1.0.0-rc.1", "1.0.0-beta.11")),
-    ?assertMatch(true, not lt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
-    ?assertMatch(true, not lt("1.0.0", "1.0.0-rc.1+build.1")),
-    ?assertMatch(true, not lt("1.0.0+0.3.7", "1.0.0")),
-    ?assertMatch(true, not lt("1.3.7+build", "1.0.0+0.3.7")),
-    ?assertMatch(true, not lt("1.3.7+build.2.b8f12d7",
-                              "1.3.7+build")),
-    ?assertMatch(true, not lt("1.3.7+build.11.e0f985a",
-                              "1.3.7+build.2.b8f12d7")).
-
-gte_test() ->
-    ?assertMatch(true, gte("1.0.0-alpha",
-                           "1.0.0-alpha")),
-
-    ?assertMatch(true, gte("1",
-                           "1.0.0")),
-
-    ?assertMatch(true, gte("1.0",
-                           "1.0.0")),
-
-    ?assertMatch(true, gte("1.0.0",
-                           "1")),
-
-    ?assertMatch(true, gte("1.0.0.0",
-                           "1")),
-
-    ?assertMatch(true, gte("1.0+alpha.1",
-                           "1.0.0+alpha.1")),
-
-    ?assertMatch(true, gte("1.0-alpha.1+build.1",
-                           "1.0.0-alpha.1+build.1")),
-
-    ?assertMatch(true, gte("1.0.0-alpha.1+build.1",
-                           "1.0.0.0-alpha.1+build.1")),
-    ?assertMatch(true, gte("1.0.0-alpha.1",
-                           "1.0.0-alpha")),
-    ?assertMatch(true, gte("1.0.0-beta.2",
-                           "1.0.0-alpha.1")),
-    ?assertMatch(true, gte("1.0.0-beta.11",
-                           "1.0.0-beta.2")),
-    ?assertMatch(true, gte("aa.bb", "aa.bb")),
-    ?assertMatch(true, gte("dd", "aa")),
-    ?assertMatch(true, gte("1.0.0-rc.1", "1.0.0-beta.11")),
-    ?assertMatch(true, gte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
-    ?assertMatch(true, gte("1.0.0", "1.0.0-rc.1+build.1")),
-    ?assertMatch(true, gte("1.0.0+0.3.7", "1.0.0")),
-    ?assertMatch(true, gte("1.3.7+build", "1.0.0+0.3.7")),
-    ?assertMatch(true, gte("1.3.7+build.2.b8f12d7",
-                           "1.3.7+build")),
-    ?assertMatch(true, gte("1.3.7+build.11.e0f985a",
-                           "1.3.7+build.2.b8f12d7")),
-    ?assertMatch(true, not gte("1.0.0-alpha",
-                               "1.0.0-alpha.1")),
-    ?assertMatch(true, not gte("CC", "DD")),
-    ?assertMatch(true, not gte("1.0.0-alpha.1",
-                               "1.0.0-beta.2")),
-    ?assertMatch(true, not gte("1.0.0-beta.2",
-                               "1.0.0-beta.11")),
-    ?assertMatch(true, not gte("1.0.0-beta.11",
-                               "1.0.0-rc.1")),
-    ?assertMatch(true, not gte("1.0.0-rc.1",
-                               "1.0.0-rc.1+build.1")),
-    ?assertMatch(true, not gte("1.0.0-rc.1+build.1",
-                               "1.0.0")),
-    ?assertMatch(true, not gte("1.0.0",
-                               "1.0.0+0.3.7")),
-    ?assertMatch(true, not gte("1.0.0+0.3.7",
-                               "1.3.7+build")),
-    ?assertMatch(true, not gte("1.0.0",
-                               "1.0.0+build.1")),
-    ?assertMatch(true, not gte("1.3.7+build",
-                               "1.3.7+build.2.b8f12d7")),
-    ?assertMatch(true, not gte("1.3.7+build.2.b8f12d7",
-                               "1.3.7+build.11.e0f985a")).
-lte_test() ->
-    ?assertMatch(true, lte("1.0.0-alpha",
-                           "1.0.0-alpha.1")),
-    ?assertMatch(true, lte("1.0.0-alpha.1",
-                           "1.0.0-beta.2")),
-    ?assertMatch(true, lte("1.0.0-beta.2",
-                           "1.0.0-beta.11")),
-    ?assertMatch(true, lte("1.0.0-beta.11",
-                           "1.0.0-rc.1")),
-    ?assertMatch(true, lte("1.0.0-rc.1",
-                           "1.0.0-rc.1+build.1")),
-    ?assertMatch(true, lte("1.0.0-rc.1+build.1",
-                           "1.0.0")),
-    ?assertMatch(true, lte("1.0.0",
-                           "1.0.0+0.3.7")),
-    ?assertMatch(true, lte("1.0.0+0.3.7",
-                           "1.3.7+build")),
-    ?assertMatch(true, lte("1.3.7+build",
-                           "1.3.7+build.2.b8f12d7")),
-    ?assertMatch(true, lte("1.3.7+build.2.b8f12d7",
-                           "1.3.7+build.11.e0f985a")),
-    ?assertMatch(true, lte("1.0.0-alpha",
-                           "1.0.0-alpha")),
-    ?assertMatch(true, lte("1",
-                           "1.0.0")),
-    ?assertMatch(true, lte("1.0",
-                           "1.0.0")),
-    ?assertMatch(true, lte("1.0.0",
-                           "1")),
-    ?assertMatch(true, lte("1.0+alpha.1",
-                           "1.0.0+alpha.1")),
-    ?assertMatch(true, lte("1.0.0.0+alpha.1",
-                           "1.0.0+alpha.1")),
-    ?assertMatch(true, lte("1.0-alpha.1+build.1",
-                           "1.0.0-alpha.1+build.1")),
-    ?assertMatch(true, lte("aa","cc")),
-    ?assertMatch(true, lte("cc","cc")),
-    ?assertMatch(true, not lte("1.0.0-alpha.1",
-                              "1.0.0-alpha")),
-    ?assertMatch(true, not lte("cc", "aa")),
-    ?assertMatch(true, not lte("1.0.0-beta.2",
-                              "1.0.0-alpha.1")),
-    ?assertMatch(true, not lte("1.0.0-beta.11",
-                              "1.0.0-beta.2")),
-    ?assertMatch(true, not lte("1.0.0-rc.1", "1.0.0-beta.11")),
-    ?assertMatch(true, not lte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
-    ?assertMatch(true, not lte("1.0.0", "1.0.0-rc.1+build.1")),
-    ?assertMatch(true, not lte("1.0.0+0.3.7", "1.0.0")),
-    ?assertMatch(true, not lte("1.3.7+build", "1.0.0+0.3.7")),
-    ?assertMatch(true, not lte("1.3.7+build.2.b8f12d7",
-                              "1.3.7+build")),
-    ?assertMatch(true, not lte("1.3.7+build.11.e0f985a",
-                              "1.3.7+build.2.b8f12d7")).
-
-between_test() ->
-    ?assertMatch(true, between("1.0.0-alpha",
-                               "1.0.0-alpha.3",
-                               "1.0.0-alpha.2")),
-    ?assertMatch(true, between("1.0.0-alpha.1",
-                               "1.0.0-beta.2",
-                               "1.0.0-alpha.25")),
-    ?assertMatch(true, between("1.0.0-beta.2",
-                               "1.0.0-beta.11",
-                               "1.0.0-beta.7")),
-    ?assertMatch(true, between("1.0.0-beta.11",
-                               "1.0.0-rc.3",
-                               "1.0.0-rc.1")),
-    ?assertMatch(true, between("1.0.0-rc.1",
-                               "1.0.0-rc.1+build.3",
-                               "1.0.0-rc.1+build.1")),
-
-    ?assertMatch(true, between("1.0.0.0-rc.1",
-                               "1.0.0-rc.1+build.3",
-                               "1.0.0-rc.1+build.1")),
-    ?assertMatch(true, between("1.0.0-rc.1+build.1",
-                               "1.0.0",
-                               "1.0.0-rc.33")),
-    ?assertMatch(true, between("1.0.0",
-                               "1.0.0+0.3.7",
-                               "1.0.0+0.2")),
-    ?assertMatch(true, between("1.0.0+0.3.7",
-                               "1.3.7+build",
-                               "1.2")),
-    ?assertMatch(true, between("1.3.7+build",
-                               "1.3.7+build.2.b8f12d7",
-                               "1.3.7+build.1")),
-    ?assertMatch(true, between("1.3.7+build.2.b8f12d7",
-                               "1.3.7+build.11.e0f985a",
-                               "1.3.7+build.10.a36faa")),
-    ?assertMatch(true, between("1.0.0-alpha",
-                               "1.0.0-alpha",
-                               "1.0.0-alpha")),
-    ?assertMatch(true, between("1",
-                               "1.0.0",
-                               "1.0.0")),
-    ?assertMatch(true, between("1.0",
-                               "1.0.0",
-                               "1.0.0")),
-
-    ?assertMatch(true, between("1.0",
-                               "1.0.0.0",
-                               "1.0.0.0")),
-    ?assertMatch(true, between("1.0.0",
-                               "1",
-                               "1")),
-    ?assertMatch(true, between("1.0+alpha.1",
-                               "1.0.0+alpha.1",
-                               "1.0.0+alpha.1")),
-    ?assertMatch(true, between("1.0-alpha.1+build.1",
-                               "1.0.0-alpha.1+build.1",
-                               "1.0.0-alpha.1+build.1")),
-    ?assertMatch(true, between("aaa",
-                               "ddd",
-                               "cc")),
-    ?assertMatch(true, not between("1.0.0-alpha.1",
-                                   "1.0.0-alpha.22",
-                                   "1.0.0")),
-    ?assertMatch(true, not between("1.0.0",
-                                   "1.0.0-alpha.1",
-                                   "2.0")),
-    ?assertMatch(true, not between("1.0.0-beta.1",
-                                   "1.0.0-beta.11",
-                                   "1.0.0-alpha")),
-    ?assertMatch(true, not between("1.0.0-beta.11", "1.0.0-rc.1",
-                                   "1.0.0-rc.22")),
-    ?assertMatch(true, not between("aaa", "ddd", "zzz")).
-
-pes_test() ->
-    ?assertMatch(true, pes("2.6.0", "2.6")),
-    ?assertMatch(true, pes("2.7", "2.6")),
-    ?assertMatch(true, pes("2.8", "2.6")),
-    ?assertMatch(true, pes("2.9", "2.6")),
-    ?assertMatch(true, pes("A.B", "A.A")),
-    ?assertMatch(true, not pes("3.0.0", "2.6")),
-    ?assertMatch(true, not pes("2.5", "2.6")),
-    ?assertMatch(true, pes("2.6.5", "2.6.5")),
-    ?assertMatch(true, pes("2.6.6", "2.6.5")),
-    ?assertMatch(true, pes("2.6.7", "2.6.5")),
-    ?assertMatch(true, pes("2.6.8", "2.6.5")),
-    ?assertMatch(true, pes("2.6.9", "2.6.5")),
-    ?assertMatch(true, pes("2.6.0.9", "2.6.0.5")),
-    ?assertMatch(true, not pes("2.7", "2.6.5")),
-    ?assertMatch(true, not pes("2.1.7", "2.1.6.5")),
-    ?assertMatch(true, not pes("A.A", "A.B")),
-    ?assertMatch(true, not pes("2.5", "2.6.5")).
-
-version_format_test() ->
-    ?assertEqual(["1", [], []], format({1, {[],[]}})),
-    ?assertEqual(["1", ".", "2", ".", "34", [], []], format({{1,2,34},{[],[]}})),
-    ?assertEqual(<<"a">>, erlang:iolist_to_binary(format({<<"a">>, {[],[]}}))),
-    ?assertEqual(<<"a.b">>, erlang:iolist_to_binary(format({{<<"a">>,<<"b">>}, {[],[]}}))),
-    ?assertEqual(<<"1">>, erlang:iolist_to_binary(format({1, {[],[]}}))),
-    ?assertEqual(<<"1.2">>, erlang:iolist_to_binary(format({{1,2}, {[],[]}}))),
-    ?assertEqual(<<"1.2.2">>, erlang:iolist_to_binary(format({{1,2,2}, {[],[]}}))),
-    ?assertEqual(<<"1.99.2">>, erlang:iolist_to_binary(format({{1,99,2}, {[],[]}}))),
-    ?assertEqual(<<"1.99.2-alpha">>, erlang:iolist_to_binary(format({{1,99,2}, {[<<"alpha">>],[]}}))),
-    ?assertEqual(<<"1.99.2-alpha.1">>, erlang:iolist_to_binary(format({{1,99,2}, {[<<"alpha">>,1], []}}))),
-    ?assertEqual(<<"1.99.2+build.1.a36">>,
-                 erlang:iolist_to_binary(format({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}}))),
-    ?assertEqual(<<"1.99.2.44+build.1.a36">>,
-                 erlang:iolist_to_binary(format({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}}))),
-    ?assertEqual(<<"1.99.2-alpha.1+build.1.a36">>,
-                 erlang:iolist_to_binary(format({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
-    ?assertEqual(<<"1">>, erlang:iolist_to_binary(format({1, {[],[]}}))).
-
--endif.
diff --git a/src/ec_semver_parser.erl b/src/ec_semver_parser.erl
index dfd9eda..c2fe186 100644
--- a/src/ec_semver_parser.erl
+++ b/src/ec_semver_parser.erl
@@ -12,7 +12,7 @@
 -define(p_zero_or_more,true).
 
 
--compile(export_all).
+
 -spec file(file:name()) -> any().
 file(Filename) -> case file:read_file(Filename) of {ok,Bin} -> parse(Bin); Err -> Err end.
 
@@ -44,11 +44,10 @@ parse(Input) when is_binary(Input) ->
 
 -spec 'alpha_part'(input(), index()) -> parse_result().
 'alpha_part'(Input, Index) ->
-  p(Input, Index, 'alpha_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[A-Za-z0-9]">>)))(I,D) end, fun(Node, _Idx) ->erlang:iolist_to_binary(Node) end).
+  p(Input, Index, 'alpha_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[A-Za-z0-9-]">>)))(I,D) end, fun(Node, _Idx) ->erlang:iolist_to_binary(Node) end).
 
 
 transform(_,Node,_Index) -> Node.
--file("peg_includes.hrl", 1).
 -type index() :: {{line, pos_integer()}, {column, pos_integer()}}.
 -type input() :: binary().
 -type parse_failure() :: {fail, term()}.
diff --git a/src/ec_talk.erl b/src/ec_talk.erl
index 5e24f2c..8c3a105 100644
--- a/src/ec_talk.erl
+++ b/src/ec_talk.erl
@@ -39,6 +39,11 @@
          say/1,
          say/2]).
 
+-ifdef(TEST).
+-export([get_boolean/1,
+	 get_integer/1]).
+-endif.
+
 -export_type([prompt/0,
               type/0,
               supported/0]).
@@ -75,7 +80,7 @@ ask(Prompt) ->
 ask_default(Prompt, Default) ->
     ask_convert(Prompt, fun get_string/1, string, Default).
 
-%% @doc Asks the user to respond to the prompt. Trys to return the
+%% @doc Asks the user to respond to the prompt. Tries to return the
 %% value in the format specified by 'Type'.
 -spec ask(prompt(), type()) ->  supported().
 ask(Prompt, boolean) ->
@@ -85,7 +90,7 @@ ask(Prompt, number) ->
 ask(Prompt, string) ->
     ask_convert(Prompt, fun get_string/1, string, none).
 
-%% @doc Asks the user to respond to the prompt. Trys to return the
+%% @doc Asks the user to respond to the prompt. Tries to return the
 %% value in the format specified by 'Type'.
 -spec ask_default(prompt(), type(), supported()) ->  supported().
 ask_default(Prompt, boolean, Default)  ->
@@ -127,7 +132,7 @@ ask_convert(Prompt, TransFun, Type,  Default) ->
                                                            Default ->
                                                                [" (", io_lib:format("~p", [Default]) , ")"]
                                                        end, "> "])),
-    Data = string:strip(string:strip(io:get_line(NewPrompt)), both, $\n),
+    Data = string:trim(string:trim(io:get_line(NewPrompt)), both, [$\n]),
     Ret = TransFun(Data),
     case Ret of
         no_data ->
@@ -145,7 +150,7 @@ ask_convert(Prompt, TransFun, Type,  Default) ->
             Ret
     end.
 
-%% @doc Trys to translate the result into a boolean
+%% @doc Tries to translate the result into a boolean
 -spec get_boolean(string()) -> boolean().
 get_boolean([]) ->
     no_data;
@@ -172,7 +177,7 @@ get_boolean([$N | _]) ->
 get_boolean(_) ->
     no_clue.
 
-%% @doc Trys to translate the result into an integer
+%% @doc Tries to translate the result into an integer
 -spec get_integer(string()) -> integer().
 get_integer([]) ->
     no_data;
@@ -196,26 +201,3 @@ get_string(String) ->
         false ->
             no_clue
     end.
-
-%%%====================================================================
-%%% tests
-%%%====================================================================
--ifdef(DEV_ONLY).
--include_lib("eunit/include/eunit.hrl").
-
-general_test_() ->
-    [?_test(42 == get_integer("42")),
-     ?_test(500211 == get_integer("500211")),
-     ?_test(1234567890 == get_integer("1234567890")),
-     ?_test(12345678901234567890 == get_integer("12345678901234567890")),
-     ?_test(true == get_boolean("true")),
-     ?_test(false == get_boolean("false")),
-     ?_test(true == get_boolean("Ok")),
-     ?_test(true == get_boolean("ok")),
-     ?_test(true == get_boolean("Y")),
-     ?_test(true == get_boolean("y")),
-     ?_test(false == get_boolean("False")),
-     ?_test(false == get_boolean("No")),
-     ?_test(false == get_boolean("no"))].
-
--endif.
diff --git a/src/ec_vsn.erl b/src/ec_vsn.erl
index 2f38090..e407b9f 100644
--- a/src/ec_vsn.erl
+++ b/src/ec_vsn.erl
@@ -27,24 +27,9 @@
 %% however you should not rely on the internal representation here
 -type t() :: #t{}.
 
--ifdef(have_callback_support).
-
 -callback new() -> any().
 -callback vsn(any()) -> {ok, string()} | {error, Reason::any()}.
 
--else.
-
-%% In the case where R14 or lower is being used to compile the system
-%% we need to export a behaviour info
--export([behaviour_info/1]).
--spec behaviour_info(atom()) -> [{atom(), arity()}] | undefined.
-behaviour_info(callbacks) ->
-    [{new, 0},
-     {vsn, 1}];
-behaviour_info(_Other) ->
-    undefined.
--endif.
-
 %%%===================================================================
 %%% API
 %%%===================================================================
diff --git a/src/erlware_commons.app.src b/src/erlware_commons.app.src
index 0ccc457..7709d81 100644
--- a/src/erlware_commons.app.src
+++ b/src/erlware_commons.app.src
@@ -1,13 +1,11 @@
-%% -*- mode: Erlang; fill-column: 75; comment-column: 50; -*-
-{application, erlware_commons,
- [{description, "Additional standard library for Erlang"},
-  {vsn, "0.13.0"},
-  {modules, []},
-  {registered, []},
-  {applications, [kernel, stdlib]},
-
-  {contributors,["Eric Merritt", "Tristan Sloughter",
-                 "Jordan Wilberding", "Martin Logan"]},
-  {licenses, ["Apache"]},
-  {links, [{"Github","https://github.com/erlware/erlware_commons"}]}
- ]}.
+{application,erlware_commons,
+             [{description,"Additional standard library for Erlang"},
+              {vsn,"git"},
+              {modules,[]},
+              {registered,[]},
+              {applications,[kernel,stdlib,cf]},
+              {maintainers,["Eric Merritt","Tristan Sloughter",
+                            "Jordan Wilberding","Martin Logan"]},
+              {licenses,["Apache", "MIT"]},
+              {links,[{"Github",
+                       "https://github.com/erlware/erlware_commons"}]}]}.
diff --git a/test/ec_cmd_log_tests.erl b/test/ec_cmd_log_tests.erl
new file mode 100644
index 0000000..f1d1181
--- /dev/null
+++ b/test/ec_cmd_log_tests.erl
@@ -0,0 +1,39 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_cmd_log_tests).
+
+-include("include/ec_cmd_log.hrl").
+-include("src/ec_cmd_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+should_test() ->
+    ErrorLogState = ec_cmd_log:new(error),
+    ?assertMatch(true, ec_cmd_log:should(ErrorLogState, ?EC_ERROR)),
+    ?assertMatch(true, not ec_cmd_log:should(ErrorLogState, ?EC_INFO)),
+    ?assertMatch(true, not ec_cmd_log:should(ErrorLogState, ?EC_DEBUG)),
+    ?assertEqual(?EC_ERROR, ec_cmd_log:log_level(ErrorLogState)),
+    ?assertEqual(error, ec_cmd_log:atom_log_level(ErrorLogState)),
+
+    InfoLogState = ec_cmd_log:new(info),
+    ?assertMatch(true, ec_cmd_log:should(InfoLogState, ?EC_ERROR)),
+    ?assertMatch(true, ec_cmd_log:should(InfoLogState, ?EC_INFO)),
+    ?assertMatch(true, not ec_cmd_log:should(InfoLogState, ?EC_DEBUG)),
+    ?assertEqual(?EC_INFO, ec_cmd_log:log_level(InfoLogState)),
+    ?assertEqual(info, ec_cmd_log:atom_log_level(InfoLogState)),
+
+    DebugLogState = ec_cmd_log:new(debug),
+    ?assertMatch(true, ec_cmd_log:should(DebugLogState, ?EC_ERROR)),
+    ?assertMatch(true, ec_cmd_log:should(DebugLogState, ?EC_INFO)),
+    ?assertMatch(true, ec_cmd_log:should(DebugLogState, ?EC_DEBUG)),
+    ?assertEqual(?EC_DEBUG, ec_cmd_log:log_level(DebugLogState)),
+    ?assertEqual(debug, ec_cmd_log:atom_log_level(DebugLogState)).
+
+
+no_color_test() ->
+    LogState = ec_cmd_log:new(debug, command_line, none),
+    ?assertEqual("test",
+                 ec_cmd_log:colorize(LogState, ?RED, true, "test")).
+
+color_test() ->
+    LogState = ec_cmd_log:new(debug, command_line, high),
+    ?assertEqual("\e[1;31m===> test\e[0m",
+                 ec_cmd_log:colorize(LogState, ?RED, true, "test")).
diff --git a/test/ec_cnv_tests.erl b/test/ec_cnv_tests.erl
new file mode 100644
index 0000000..6bbad6e
--- /dev/null
+++ b/test/ec_cnv_tests.erl
@@ -0,0 +1,28 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_cnv_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+to_integer_test() ->
+    ?assertError(badarg, ec_cnv:to_integer(1.5, strict)).
+
+to_float_test() ->
+    ?assertError(badarg, ec_cnv:to_float(10, strict)).
+
+to_atom_test() ->
+    ?assertMatch(true, ec_cnv:to_atom("true")),
+    ?assertMatch(true, ec_cnv:to_atom(<<"true">>)),
+    ?assertMatch(false, ec_cnv:to_atom(<<"false">>)),
+    ?assertMatch(false, ec_cnv:to_atom(false)),
+    ?assertError(badarg, ec_cnv:to_atom("hello_foo_bar_baz")),
+
+    S = erlang:list_to_atom("1"),
+    ?assertMatch(S, ec_cnv:to_atom(1)).
+
+to_boolean_test()->
+    ?assertMatch(true, ec_cnv:to_boolean(<<"true">>)),
+    ?assertMatch(true, ec_cnv:to_boolean("true")),
+    ?assertMatch(true, ec_cnv:to_boolean(true)),
+    ?assertMatch(false, ec_cnv:to_boolean(<<"false">>)),
+    ?assertMatch(false, ec_cnv:to_boolean("false")),
+    ?assertMatch(false, ec_cnv:to_boolean(false)).
diff --git a/test/ec_dictionary_proper.erl b/test/ec_dictionary_proper.erl
deleted file mode 100644
index a455e90..0000000
--- a/test/ec_dictionary_proper.erl
+++ /dev/null
@@ -1,226 +0,0 @@
-%% compile with
-%% erl -pz ebin --make
-%% start test with
-%%   erl -pz ebin -pz test
-%%   proper:module(ec_dictionary_proper).
--module(ec_dictionary_proper).
-
--ifdef(DEV_ONLY).
-
--export([my_dict/0, dict/1, sym_dict/0, sym_dict/1, gb_tree/0, gb_tree/1, sym_dict2/0]).
-
--include_lib("proper/include/proper.hrl").
-
-
-%%------------------------------------------------------------------------------
-%% Properties
-%%------------------------------------------------------------------------------
-
-prop_size_increases_with_new_key() ->
-    ?FORALL({Dict,K}, {sym_dict(),integer()},
-            begin
-                Size = ec_dictionary:size(Dict),
-                case ec_dictionary:has_key(K,Dict) of
-                    true ->
-                        Size == ec_dictionary:size(ec_dictionary:add(K,0,Dict));
-                    false ->
-                        (Size + 1) == ec_dictionary:size(ec_dictionary:add(K,0,Dict))
-                end
-            end).
-
-prop_size_decrease_when_removing() ->
-    ?FORALL({Dict,K}, {sym_dict(),integer()},
-            begin
-                Size = ec_dictionary:size(Dict),
-                case ec_dictionary:has_key(K,Dict) of
-                    false ->
-                        Size == ec_dictionary:size(ec_dictionary:remove(K,Dict));
-                    true ->
-                        (Size - 1) == ec_dictionary:size(ec_dictionary:remove(K,Dict))
-                end
-            end).
-
-prop_get_after_add_returns_correct_value() ->
-    ?FORALL({Dict,K,V}, {sym_dict(),key(),value()},
-            begin
-                try ec_dictionary:get(K,ec_dictionary:add(K,V,Dict)) of
-                    V ->
-                        true;
-                    _ ->
-                        false
-                catch
-                    _:_ ->
-                        false
-                end
-            end).
-
-prop_get_default_returns_correct_value() ->
-    ?FORALL({Dict,K1,K2,V,Default},
-            {sym_dict(),key(),key(),value(),value()},
-            begin
-                NewDict = ec_dictionary:add(K1,V, Dict),
-                %% In the unlikely event that keys that are the same
-                %% are generated
-                case ec_dictionary:has_key(K2, NewDict) of
-                    true ->
-                        true;
-                    false ->
-                        ec_dictionary:get(K2, Default, NewDict) == Default
-                end
-            end).
-
-
-prop_add_does_not_change_values_for_other_keys() ->
-    ?FORALL({Dict,K,V},  {sym_dict(),key(),value()},
-            begin
-                Keys = ec_dictionary:keys(Dict),
-                ?IMPLIES(not lists:member(K,Keys),
-                         begin
-                             Dict2 = ec_dictionary:add(K,V,Dict),
-                             try lists:all(fun(B) -> B end,
-                                           [ ec_dictionary:get(Ka,Dict) ==
-                                                 ec_dictionary:get(Ka,Dict2) ||
-                                               Ka <- Keys ]) of
-                                 Bool -> Bool
-                             catch
-                                 throw:not_found -> true
-                             end
-                         end)
-            end).
-
-
-
-prop_key_is_present_after_add() ->
-    ?FORALL({Dict,K,V}, {sym_dict(),integer(),integer()},
-            begin
-                ec_dictionary:has_key(K,ec_dictionary:add(K,V,Dict))        end).
-
-prop_value_is_present_after_add() ->
-    ?FORALL({Dict,K,V}, {sym_dict(),integer(),integer()},
-            begin
-                ec_dictionary:has_value(V,ec_dictionary:add(K,V,Dict))
-            end).
-
-prop_to_list_matches_get() ->
-    ?FORALL(Dict,sym_dict(),
-            begin
-                %% Dict = eval(SymDict),
-                %% io:format("SymDict: ~p~n",[proper_symb:symbolic_seq(SymDict)]),
-                ToList = ec_dictionary:to_list(Dict),
-                %% io:format("ToList:~p~n",[ToList]),
-                GetList =
-                    try [ {K,ec_dictionary:get(K,Dict)} || {K,_V} <- ToList ] of
-                        List -> List
-                    catch
-                        throw:not_found -> key_not_found
-                    end,
-                %% io:format("~p == ~p~n",[ToList,GetList]),
-                lists:sort(ToList) == lists:sort(GetList)
-            end).
-
-prop_value_changes_after_update() ->
-    ?FORALL({Dict, K1, V1, V2},
-            {sym_dict(),
-             key(), value(), value()},
-            begin
-                Dict1 = ec_dictionary:add(K1, V1, Dict),
-                Dict2 = ec_dictionary:add(K1, V2, Dict1),
-                V1 == ec_dictionary:get(K1, Dict1) andalso
-                    V2 == ec_dictionary:get(K1, Dict2)
-            end).
-
-prop_remove_removes_only_one_key() ->
-    ?FORALL({Dict,K},
-            {sym_dict(),key()},
-            begin
-                {KeyGone,Dict2} = case ec_dictionary:has_key(K,Dict) of
-                                      true ->
-                                          D2 = ec_dictionary:remove(K,Dict),
-                                          {ec_dictionary:has_key(K,D2) == false,
-                                           D2};
-                                      false ->
-                                          {true,ec_dictionary:remove(K,Dict)}
-                                  end,
-                OtherEntries = [ KV || {K1,_} = KV <- ec_dictionary:to_list(Dict),
-                                       K1 /= K ],
-                KeyGone andalso
-                    lists:sort(OtherEntries) == lists:sort(ec_dictionary:to_list(Dict2))
-            end).
-
-prop_from_list() ->
-    ?FORALL({Dict,DictType},
-            {sym_dict(),dictionary()},
-            begin
-                List = ec_dictionary:to_list(Dict),
-                D2 = ec_dictionary:from_list(DictType,List),
-                List2 = ec_dictionary:to_list(D2),
-                lists:sort(List) == lists:sort(List2)
-            end).
-
-
-%%-----------------------------------------------------------------------------
-%% Generators
-%%-----------------------------------------------------------------------------
-
-key() -> union([integer(),atom()]).
-
-value() -> union([integer(),atom(),binary(),boolean(),string()]).
-
-
-my_dict() ->
-    ?SIZED(N,dict(N)).
-
-
-dict(0) ->
-    ec_dictionary:new(ec_gb_trees);
-dict(N) ->
-    ?LET(D,dict(N-1),
-         frequency([
-                    {1, dict(0)},
-                    {3, ec_dictionary:remove(integer(),D)},
-                    {6, ec_dictionary:add(integer(),integer(),D)}
-                  ])).
-
-sym_dict() ->
-    ?SIZED(N,sym_dict(N)).
-
-%% This symbolic generator will create a random instance of a ec_dictionary
-%% that will be used in the properties.
-sym_dict(0) ->
-    ?LET(Dict,dictionary(),
-         {'$call',ec_dictionary,new,[Dict]});
-sym_dict(N) ->
-    ?LAZY(
-       frequency([
-                  {1, sym_dict(0)},
-                  {3, {'$call',ec_dictionary,remove,[key(),sym_dict(N-1)]}},
-                  {6, {'$call',ec_dictionary,add,[value(),value(),sym_dict(N-1)]}}
-                 ])
-      ).
-
-dictionary() ->
-    union([ec_gb_trees,ec_assoc_list,ec_dict,ec_orddict]).
-
-sym_dict2() ->
-    ?SIZED(N,sym_dict2(N)).
-
-sym_dict2(0) ->
-    {call,ec_dictionary,new,[ec_gb_trees]};
-sym_dict2(N) ->
-    D = dict(N-1),
-    frequency([
-               {1, {call,ec_dictionary,remove,[integer(),D]}},
-               {2, {call,ec_dictionary,add,[integer(),integer(),D]}}
-               ]).
-
-
-%% For the tutorial.
-gb_tree() ->
-    ?SIZED(N,gb_tree(N)).
-
-gb_tree(0) ->
-    gb_trees:empty();
-gb_tree(N) ->
-    gb_trees:enter(key(),value(),gb_tree(N-1)).
-
--endif.
diff --git a/test/ec_file_tests.erl b/test/ec_file_tests.erl
new file mode 100644
index 0000000..885f3dc
--- /dev/null
+++ b/test/ec_file_tests.erl
@@ -0,0 +1,84 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_file_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+setup_test() ->
+    Dir = ec_file:insecure_mkdtemp(),
+    ec_file:mkdir_path(Dir),
+    ?assertMatch(false, ec_file:is_symlink(Dir)),
+    ?assertMatch(true, filelib:is_dir(Dir)).
+
+md5sum_test() ->
+    ?assertMatch("cfcd208495d565ef66e7dff9f98764da", ec_file:md5sum("0")).
+
+sha1sum_test() ->
+    ?assertMatch("b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", ec_file:sha1sum("0")).
+
+file_test() ->
+    Dir = ec_file:insecure_mkdtemp(),
+    TermFile = filename:join(Dir, "ec_file/dir/file.term"),
+    TermFileCopy = filename:join(Dir, "ec_file/dircopy/file.term"),
+    filelib:ensure_dir(TermFile),
+    filelib:ensure_dir(TermFileCopy),
+    ec_file:write_term(TermFile, "term"),
+    ?assertMatch({ok, <<"\"term\". ">>}, ec_file:read(TermFile)),
+    ec_file:copy(filename:dirname(TermFile),
+         filename:dirname(TermFileCopy),
+         [recursive]).
+
+teardown_test() ->
+    Dir = ec_file:insecure_mkdtemp(),
+    ec_file:remove(Dir, [recursive]),
+    ?assertMatch(false, filelib:is_dir(Dir)).
+
+setup_base_and_target() ->
+    BaseDir = ec_file:insecure_mkdtemp(),
+    DummyContents = <<"This should be deleted">>,
+    SourceDir = filename:join([BaseDir, "source"]),
+    ok = file:make_dir(SourceDir),
+    Name1 = filename:join([SourceDir, "fileone"]),
+    Name2 = filename:join([SourceDir, "filetwo"]),
+    Name3 = filename:join([SourceDir, "filethree"]),
+    NoName = filename:join([SourceDir, "noname"]),
+
+    ok = file:write_file(Name1, DummyContents),
+    ok = file:write_file(Name2, DummyContents),
+    ok = file:write_file(Name3, DummyContents),
+    ok = file:write_file(NoName, DummyContents),
+    {BaseDir, SourceDir, {Name1, Name2, Name3, NoName}}.
+
+exists_test() ->
+    BaseDir = ec_file:insecure_mkdtemp(),
+    SourceDir = filename:join([BaseDir, "source1"]),
+    NoName = filename:join([SourceDir, "noname"]),
+    ok = file:make_dir(SourceDir),
+    Name1 = filename:join([SourceDir, "fileone"]),
+    ok = file:write_file(Name1, <<"Testn">>),
+    ?assertMatch(true, ec_file:exists(Name1)),
+    ?assertMatch(false, ec_file:exists(NoName)).
+
+real_path_test() ->
+    BaseDir = "foo",
+    Dir = filename:absname(filename:join(BaseDir, "source1")),
+    LinkDir = filename:join([BaseDir, "link"]),
+    ok = ec_file:mkdir_p(Dir),
+    file:make_symlink(Dir, LinkDir),
+    ?assertEqual(Dir, ec_file:real_dir_path(LinkDir)),
+    ?assertEqual(directory, ec_file:type(Dir)),
+    ?assertEqual(symlink, ec_file:type(LinkDir)),
+    TermFile = filename:join(BaseDir, "test_file"),
+    ok = ec_file:write_term(TermFile, foo),
+    ?assertEqual(file, ec_file:type(TermFile)),
+    ?assertEqual(true, ec_file:is_symlink(LinkDir)),
+    ?assertEqual(false, ec_file:is_symlink(Dir)).
+
+find_test() ->
+    %% Create a directory in /tmp for the test. Clean everything afterwards
+    {BaseDir, _SourceDir, {Name1, Name2, Name3, _NoName}} = setup_base_and_target(),
+    Result = ec_file:find(BaseDir, "file[a-z]+\$"),
+    ?assertMatch(3, erlang:length(Result)),
+    ?assertEqual(true, lists:member(Name1, Result)),
+    ?assertEqual(true, lists:member(Name2, Result)),
+    ?assertEqual(true, lists:member(Name3, Result)),
+    ec_file:remove(BaseDir, [recursive]).
diff --git a/test/ec_gb_trees_tests.erl b/test/ec_gb_trees_tests.erl
new file mode 100644
index 0000000..2c0ee12
--- /dev/null
+++ b/test/ec_gb_trees_tests.erl
@@ -0,0 +1,67 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_gb_trees_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% For me unit testing initially is about covering the obvious case. A
+%% check to make sure that what you expect the tested functionality to
+%% do, it actually does. As time goes on and people detect bugs you
+%% add tests for those specific problems to the unit test suit.
+%%
+%% However, when getting started you can only test your basic
+%% expectations. So here are the expectations I have for the add
+%% functionality.
+%%
+%% 1) I can put arbitrary terms into the dictionary as keys
+%% 2) I can put arbitrary terms into the dictionary as values
+%% 3) When I put a value in the dictionary by a key, I can retrieve
+%% that same value
+%% 4) When I put a different value in the dictionary by key it does
+%% not change other key value pairs.
+%% 5) When I update a value the new value in available by the new key
+%% 6) When a value does not exist a not found exception is created
+
+add_test() ->
+    Dict0 = ec_dictionary:new(ec_gb_trees),
+
+    Key1 = foo,
+    Key2 = [1, 3],
+    Key3 = {"super"},
+    Key4 = <<"fabulous">>,
+    Key5 = {"Sona", 2, <<"Zuper">>},
+
+    Value1 = Key5,
+    Value2 = Key4,
+    Value3 = Key2,
+    Value4 = Key3,
+    Value5 = Key1,
+
+    Dict01 = ec_dictionary:add(Key1, Value1, Dict0),
+    Dict02 = ec_dictionary:add(Key3, Value3,
+                               ec_dictionary:add(Key2, Value2,
+                                                 Dict01)),
+    Dict1 =
+        ec_dictionary:add(Key5, Value5,
+                          ec_dictionary:add(Key4, Value4,
+                                            Dict02)),
+
+    ?assertMatch(Value1, ec_dictionary:get(Key1, Dict1)),
+    ?assertMatch(Value2, ec_dictionary:get(Key2, Dict1)),
+    ?assertMatch(Value3, ec_dictionary:get(Key3, Dict1)),
+    ?assertMatch(Value4, ec_dictionary:get(Key4, Dict1)),
+    ?assertMatch(Value5, ec_dictionary:get(Key5, Dict1)),
+
+
+    Dict2 = ec_dictionary:add(Key3, Value5,
+                              ec_dictionary:add(Key2, Value4, Dict1)),
+
+
+    ?assertMatch(Value1, ec_dictionary:get(Key1, Dict2)),
+    ?assertMatch(Value4, ec_dictionary:get(Key2, Dict2)),
+    ?assertMatch(Value5, ec_dictionary:get(Key3, Dict2)),
+    ?assertMatch(Value4, ec_dictionary:get(Key4, Dict2)),
+    ?assertMatch(Value5, ec_dictionary:get(Key5, Dict2)),
+
+
+    ?assertThrow(not_found, ec_dictionary:get(should_blow_up, Dict2)),
+    ?assertThrow(not_found, ec_dictionary:get("This should blow up too",
+                                              Dict2)).
diff --git a/test/ec_git_vsn_tests.erl b/test/ec_git_vsn_tests.erl
new file mode 100644
index 0000000..0d2efe1
--- /dev/null
+++ b/test/ec_git_vsn_tests.erl
@@ -0,0 +1,13 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_git_vsn_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+parse_tags_test() ->
+    ?assertEqual({undefined, ""}, ec_git_vsn:parse_tags("a.b.c")).
+
+get_patch_count_test() ->
+    ?assertEqual(0, ec_git_vsn:get_patch_count("a.b.c")).
+
+collect_default_refcount_test() ->
+    ?assertMatch({"", _, _}, ec_git_vsn:collect_default_refcount("a.b.c")).
diff --git a/test/ec_lists_tests.erl b/test/ec_lists_tests.erl
new file mode 100644
index 0000000..f6f4025
--- /dev/null
+++ b/test/ec_lists_tests.erl
@@ -0,0 +1,172 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_lists_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+find1_test() ->
+    TestData = [1, 2, 3, 4, 5, 6],
+    Result = ec_lists:find(fun(5) ->
+                          true;
+                     (_) ->
+                          false
+                  end,
+                  TestData),
+    ?assertMatch({ok, 5}, Result),
+
+    Result2 = ec_lists:find(fun(37) ->
+                           true;
+                      (_) ->
+                           false
+                   end,
+                   TestData),
+    ?assertMatch(error, Result2).
+
+find2_test() ->
+    TestData = ["one", "two", "three", "four", "five", "six"],
+    Result = ec_lists:find(fun("five") ->
+                          true;
+                     (_) ->
+                          false
+                  end,
+                  TestData),
+    ?assertMatch({ok, "five"}, Result),
+
+    Result2 = ec_lists:find(fun(super_duper) ->
+                           true;
+                      (_) ->
+                           false
+                   end,
+                   TestData),
+    ?assertMatch(error, Result2).
+
+find3_test() ->
+    TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
+                {"six", 6}],
+    Result = ec_lists:find(fun({"one", 1}) ->
+                          true;
+                     (_) ->
+                          false
+                  end,
+                  TestData),
+    ?assertMatch({ok, {"one", 1}}, Result),
+
+    Result2 = ec_lists:find(fun([fo, bar, baz]) ->
+                           true;
+                      ({"onehundred", 100}) ->
+                           true;
+                      (_) ->
+                           false
+                   end,
+                   TestData),
+    ?assertMatch(error, Result2).
+
+fetch1_test() ->
+    TestData = [1, 2, 3, 4, 5, 6],
+    Result = ec_lists:fetch(fun(5) ->
+                           true;
+                      (_) ->
+                           false
+                   end,
+                   TestData),
+    ?assertMatch(5, Result),
+
+    ?assertThrow(not_found,
+                 ec_lists:fetch(fun(37) ->
+                               true;
+                          (_) ->
+                               false
+                       end,
+                       TestData)).
+
+fetch2_test() ->
+    TestData = ["one", "two", "three", "four", "five", "six"],
+    Result = ec_lists:fetch(fun("five") ->
+                           true;
+                      (_) ->
+                           false
+                   end,
+                   TestData),
+    ?assertMatch("five", Result),
+
+    ?assertThrow(not_found,
+                 ec_lists:fetch(fun(super_duper) ->
+                               true;
+                          (_) ->
+                               false
+                       end,
+                       TestData)).
+
+fetch3_test() ->
+    TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
+                {"six", 6}],
+    Result = ec_lists:fetch(fun({"one", 1}) ->
+                           true;
+                      (_) ->
+                           false
+                   end,
+                   TestData),
+    ?assertMatch({"one", 1}, Result),
+
+    ?assertThrow(not_found,
+                 ec_lists:fetch(fun([fo, bar, baz]) ->
+                               true;
+                          ({"onehundred", 100}) ->
+                               true;
+                          (_) ->
+                               false
+                       end,
+                       TestData)).
+
+search1_test() ->
+    TestData = [1, 2, 3, 4, 5, 6],
+    Result = ec_lists:search(fun(5) ->
+                            {ok, 5};
+                       (_) ->
+                            not_found
+                    end,
+                    TestData),
+    ?assertMatch({ok, 5, 5}, Result),
+
+    Result2 = ec_lists:search(fun(37) ->
+                             {ok, 37};
+                        (_) ->
+                             not_found
+                     end,
+                     TestData),
+    ?assertMatch(not_found, Result2).
+
+search2_test() ->
+    TestData = [1, 2, 3, 4, 5, 6],
+    Result = ec_lists:search(fun(1) ->
+                            {ok, 10};
+                       (_) ->
+                            not_found
+                    end,
+                    TestData),
+    ?assertMatch({ok, 10, 1}, Result),
+
+    Result2 = ec_lists:search(fun(6) ->
+                             {ok, 37};
+                        (_) ->
+                             not_found
+                     end,
+                     TestData),
+    ?assertMatch({ok, 37, 6}, Result2).
+
+search3_test() ->
+    TestData = [1, 2, 3, 4, 5, 6],
+    Result = ec_lists:search(fun(10) ->
+                            {ok, 10};
+                       (_) ->
+                            not_found
+                    end,
+                    TestData),
+    ?assertMatch(not_found, Result),
+
+    Result2 = ec_lists:search(fun(-1) ->
+                             {ok, 37};
+                        (_) ->
+                             not_found
+                     end,
+                     TestData),
+    ?assertMatch(not_found, Result2).
diff --git a/test/ec_plists_tests.erl b/test/ec_plists_tests.erl
index ff5d2e9..3f945e9 100644
--- a/test/ec_plists_tests.erl
+++ b/test/ec_plists_tests.erl
@@ -1,8 +1,6 @@
 %%% @copyright Erlware, LLC.
 -module(ec_plists_tests).
 
--ifdef(DEV_ONLY).
-
 -include_lib("eunit/include/eunit.hrl").
 
 %%%===================================================================
@@ -76,4 +74,11 @@ ftmap_bad_test() ->
     ?assertMatch([{value, 1}, {error,{throw,test_exception}}, {value, 3},
                   {value, 4}, {value, 5}] , Results).
 
--endif.
+external_down_message_test() ->
+    erlang:spawn_monitor(fun() -> erlang:throw(fail) end),
+    Results = ec_plists:map(fun(_) ->
+                                    ok
+                            end,
+                            lists:seq(1, 5)),
+    ?assertMatch([ok, ok, ok, ok, ok],
+                 Results).
diff --git a/test/ec_semver_tests.erl b/test/ec_semver_tests.erl
new file mode 100644
index 0000000..0d3a18a
--- /dev/null
+++ b/test/ec_semver_tests.erl
@@ -0,0 +1,447 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_semver_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+eql_test() ->
+    ?assertMatch(true, ec_semver:eql("1.0.0-alpha",
+                           "1.0.0-alpha")),
+    ?assertMatch(true, ec_semver:eql("v1.0.0-alpha",
+                           "1.0.0-alpha")),
+    ?assertMatch(true, ec_semver:eql("1",
+                           "1.0.0")),
+    ?assertMatch(true, ec_semver:eql("v1",
+                           "v1.0.0")),
+    ?assertMatch(true, ec_semver:eql("1.0",
+                           "1.0.0")),
+    ?assertMatch(true, ec_semver:eql("1.0.0",
+                           "1")),
+    ?assertMatch(true, ec_semver:eql("1.0.0.0",
+                           "1")),
+    ?assertMatch(true, ec_semver:eql("1.0+alpha.1",
+                           "1.0.0+alpha.1")),
+    ?assertMatch(true, ec_semver:eql("1.0-alpha.1+build.1",
+                           "1.0.0-alpha.1+build.1")),
+    ?assertMatch(true, ec_semver:eql("1.0-alpha.1+build.1",
+                           "1.0.0.0-alpha.1+build.1")),
+    ?assertMatch(true, ec_semver:eql("1.0-alpha.1+build.1",
+                           "v1.0.0.0-alpha.1+build.1")),
+    ?assertMatch(true, ec_semver:eql("1.0-pre-alpha.1",
+                           "1.0.0-pre-alpha.1")),
+    ?assertMatch(true, ec_semver:eql("aa", "aa")),
+    ?assertMatch(true, ec_semver:eql("AA.BB", "AA.BB")),
+    ?assertMatch(true, ec_semver:eql("BBB-super", "BBB-super")),
+    ?assertMatch(true, not ec_semver:eql("1.0.0",
+                               "1.0.1")),
+    ?assertMatch(true, not ec_semver:eql("1.0.0-alpha",
+                               "1.0.1+alpha")),
+    ?assertMatch(true, not ec_semver:eql("1.0.0+build.1",
+                               "1.0.1+build.2")),
+    ?assertMatch(true, not ec_semver:eql("1.0.0.0+build.1",
+                               "1.0.0.1+build.2")),
+    ?assertMatch(true, not ec_semver:eql("FFF", "BBB")),
+    ?assertMatch(true, not ec_semver:eql("1", "1BBBB")).
+
+gt_test() ->
+    ?assertMatch(true, ec_semver:gt("1.0.0-alpha.1",
+                          "1.0.0-alpha")),
+    ?assertMatch(true, ec_semver:gt("1.0.0.1-alpha.1",
+                          "1.0.0.1-alpha")),
+    ?assertMatch(true, ec_semver:gt("1.0.0.4-alpha.1",
+                          "1.0.0.2-alpha")),
+    ?assertMatch(true, ec_semver:gt("1.0.0.0-alpha.1",
+                          "1.0.0-alpha")),
+    ?assertMatch(true, ec_semver:gt("1.0.0-beta.2",
+                          "1.0.0-alpha.1")),
+    ?assertMatch(true, ec_semver:gt("1.0.0-beta.11",
+                          "1.0.0-beta.2")),
+    ?assertMatch(true, ec_semver:gt("1.0.0-pre-alpha.14",
+                          "1.0.0-pre-alpha.3")),
+    ?assertMatch(true, ec_semver:gt("1.0.0-beta.11",
+                          "1.0.0.0-beta.2")),
+    ?assertMatch(true, ec_semver:gt("1.0.0-rc.1", "1.0.0-beta.11")),
+    ?assertMatch(true, ec_semver:gt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
+    ?assertMatch(true, ec_semver:gt("1.0.0", "1.0.0-rc.1+build.1")),
+    ?assertMatch(true, ec_semver:gt("1.0.0+0.3.7", "1.0.0")),
+    ?assertMatch(true, ec_semver:gt("1.3.7+build", "1.0.0+0.3.7")),
+    ?assertMatch(true, ec_semver:gt("1.3.7+build.2.b8f12d7",
+                          "1.3.7+build")),
+    ?assertMatch(true, ec_semver:gt("1.3.7+build.2.b8f12d7",
+                          "1.3.7.0+build")),
+    ?assertMatch(true, ec_semver:gt("1.3.7+build.11.e0f985a",
+                          "1.3.7+build.2.b8f12d7")),
+    ?assertMatch(true, ec_semver:gt("aa.cc",
+                          "aa.bb")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0-alpha",
+                              "1.0.0-alpha.1")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0-alpha",
+                              "1.0.0.0-alpha.1")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0-alpha.1",
+                              "1.0.0-beta.2")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0-beta.2",
+                              "1.0.0-beta.11")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0-beta.11",
+                              "1.0.0-rc.1")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0-pre-alpha.3",
+                              "1.0.0-pre-alpha.14")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0-rc.1",
+                              "1.0.0-rc.1+build.1")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0-rc.1+build.1",
+                              "1.0.0")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0",
+                              "1.0.0+0.3.7")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0+0.3.7",
+                              "1.3.7+build")),
+    ?assertMatch(true, not ec_semver:gt("1.3.7+build",
+                              "1.3.7+build.2.b8f12d7")),
+    ?assertMatch(true, not ec_semver:gt("1.3.7+build.2.b8f12d7",
+                              "1.3.7+build.11.e0f985a")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0-alpha",
+                              "1.0.0-alpha")),
+    ?assertMatch(true, not ec_semver:gt("1",
+                              "1.0.0")),
+    ?assertMatch(true, not ec_semver:gt("aa.bb",
+                              "aa.bb")),
+    ?assertMatch(true, not ec_semver:gt("aa.cc",
+                              "aa.dd")),
+    ?assertMatch(true, not ec_semver:gt("1.0",
+                              "1.0.0")),
+    ?assertMatch(true, not ec_semver:gt("1.0.0",
+                              "1")),
+    ?assertMatch(true, not ec_semver:gt("1.0+alpha.1",
+                              "1.0.0+alpha.1")),
+    ?assertMatch(true, not ec_semver:gt("1.0-alpha.1+build.1",
+                              "1.0.0-alpha.1+build.1")).
+
+lt_test() ->
+    ?assertMatch(true, ec_semver:lt("1.0.0-alpha",
+                          "1.0.0-alpha.1")),
+    ?assertMatch(true, ec_semver:lt("1.0.0-alpha",
+                          "1.0.0.0-alpha.1")),
+    ?assertMatch(true, ec_semver:lt("1.0.0-alpha.1",
+                          "1.0.0-beta.2")),
+    ?assertMatch(true, ec_semver:lt("1.0.0-beta.2",
+                          "1.0.0-beta.11")),
+    ?assertMatch(true, ec_semver:lt("1.0.0-pre-alpha.3",
+                          "1.0.0-pre-alpha.14")),
+    ?assertMatch(true, ec_semver:lt("1.0.0-beta.11",
+                          "1.0.0-rc.1")),
+    ?assertMatch(true, ec_semver:lt("1.0.0.1-beta.11",
+                          "1.0.0.1-rc.1")),
+    ?assertMatch(true, ec_semver:lt("1.0.0-rc.1",
+                          "1.0.0-rc.1+build.1")),
+    ?assertMatch(true, ec_semver:lt("1.0.0-rc.1+build.1",
+                          "1.0.0")),
+    ?assertMatch(true, ec_semver:lt("1.0.0",
+                          "1.0.0+0.3.7")),
+    ?assertMatch(true, ec_semver:lt("1.0.0+0.3.7",
+                          "1.3.7+build")),
+    ?assertMatch(true, ec_semver:lt("1.3.7+build",
+                          "1.3.7+build.2.b8f12d7")),
+    ?assertMatch(true, ec_semver:lt("1.3.7+build.2.b8f12d7",
+                          "1.3.7+build.11.e0f985a")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0-alpha",
+                              "1.0.0-alpha")),
+    ?assertMatch(true, not ec_semver:lt("1",
+                              "1.0.0")),
+    ?assertMatch(true, ec_semver:lt("1",
+                          "1.0.0.1")),
+    ?assertMatch(true, ec_semver:lt("AA.DD",
+                          "AA.EE")),
+    ?assertMatch(true, not ec_semver:lt("1.0",
+                              "1.0.0")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0.0",
+                              "1")),
+    ?assertMatch(true, not ec_semver:lt("1.0+alpha.1",
+                              "1.0.0+alpha.1")),
+    ?assertMatch(true, not ec_semver:lt("AA.DD", "AA.CC")),
+    ?assertMatch(true, not ec_semver:lt("1.0-alpha.1+build.1",
+                              "1.0.0-alpha.1+build.1")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0-alpha.1",
+                              "1.0.0-alpha")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0-beta.2",
+                              "1.0.0-alpha.1")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0-beta.11",
+                              "1.0.0-beta.2")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0-pre-alpha.14",
+                              "1.0.0-pre-alpha.3")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0-rc.1", "1.0.0-beta.11")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0", "1.0.0-rc.1+build.1")),
+    ?assertMatch(true, not ec_semver:lt("1.0.0+0.3.7", "1.0.0")),
+    ?assertMatch(true, not ec_semver:lt("1.3.7+build", "1.0.0+0.3.7")),
+    ?assertMatch(true, not ec_semver:lt("1.3.7+build.2.b8f12d7",
+                              "1.3.7+build")),
+    ?assertMatch(true, not ec_semver:lt("1.3.7+build.11.e0f985a",
+                              "1.3.7+build.2.b8f12d7")).
+
+gte_test() ->
+    ?assertMatch(true, ec_semver:gte("1.0.0-alpha",
+                           "1.0.0-alpha")),
+
+    ?assertMatch(true, ec_semver:gte("1",
+                           "1.0.0")),
+
+    ?assertMatch(true, ec_semver:gte("1.0",
+                           "1.0.0")),
+
+    ?assertMatch(true, ec_semver:gte("1.0.0",
+                           "1")),
+
+    ?assertMatch(true, ec_semver:gte("1.0.0.0",
+                           "1")),
+
+    ?assertMatch(true, ec_semver:gte("1.0+alpha.1",
+                           "1.0.0+alpha.1")),
+
+    ?assertMatch(true, ec_semver:gte("1.0-alpha.1+build.1",
+                           "1.0.0-alpha.1+build.1")),
+
+    ?assertMatch(true, ec_semver:gte("1.0.0-alpha.1+build.1",
+                           "1.0.0.0-alpha.1+build.1")),
+    ?assertMatch(true, ec_semver:gte("1.0.0-alpha.1",
+                           "1.0.0-alpha")),
+    ?assertMatch(true, ec_semver:gte("1.0.0-pre-alpha.2",
+                           "1.0.0-pre-alpha")),
+    ?assertMatch(true, ec_semver:gte("1.0.0-beta.2",
+                           "1.0.0-alpha.1")),
+    ?assertMatch(true, ec_semver:gte("1.0.0-beta.11",
+                           "1.0.0-beta.2")),
+    ?assertMatch(true, ec_semver:gte("aa.bb", "aa.bb")),
+    ?assertMatch(true, ec_semver:gte("dd", "aa")),
+    ?assertMatch(true, ec_semver:gte("1.0.0-rc.1", "1.0.0-beta.11")),
+    ?assertMatch(true, ec_semver:gte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
+    ?assertMatch(true, ec_semver:gte("1.0.0", "1.0.0-rc.1+build.1")),
+    ?assertMatch(true, ec_semver:gte("1.0.0+0.3.7", "1.0.0")),
+    ?assertMatch(true, ec_semver:gte("1.3.7+build", "1.0.0+0.3.7")),
+    ?assertMatch(true, ec_semver:gte("1.3.7+build.2.b8f12d7",
+                           "1.3.7+build")),
+    ?assertMatch(true, ec_semver:gte("1.3.7+build.11.e0f985a",
+                           "1.3.7+build.2.b8f12d7")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0-alpha",
+                               "1.0.0-alpha.1")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0-pre-alpha",
+                               "1.0.0-pre-alpha.1")),
+    ?assertMatch(true, not ec_semver:gte("CC", "DD")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0-alpha.1",
+                               "1.0.0-beta.2")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0-beta.2",
+                               "1.0.0-beta.11")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0-beta.11",
+                               "1.0.0-rc.1")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0-rc.1",
+                               "1.0.0-rc.1+build.1")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0-rc.1+build.1",
+                               "1.0.0")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0",
+                               "1.0.0+0.3.7")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0+0.3.7",
+                               "1.3.7+build")),
+    ?assertMatch(true, not ec_semver:gte("1.0.0",
+                               "1.0.0+build.1")),
+    ?assertMatch(true, not ec_semver:gte("1.3.7+build",
+                               "1.3.7+build.2.b8f12d7")),
+    ?assertMatch(true, not ec_semver:gte("1.3.7+build.2.b8f12d7",
+                               "1.3.7+build.11.e0f985a")).
+lte_test() ->
+    ?assertMatch(true, ec_semver:lte("1.0.0-alpha",
+                           "1.0.0-alpha.1")),
+    ?assertMatch(true, ec_semver:lte("1.0.0-alpha.1",
+                           "1.0.0-beta.2")),
+    ?assertMatch(true, ec_semver:lte("1.0.0-beta.2",
+                           "1.0.0-beta.11")),
+    ?assertMatch(true, ec_semver:lte("1.0.0-pre-alpha.2",
+                           "1.0.0-pre-alpha.11")),
+    ?assertMatch(true, ec_semver:lte("1.0.0-beta.11",
+                           "1.0.0-rc.1")),
+    ?assertMatch(true, ec_semver:lte("1.0.0-rc.1",
+                           "1.0.0-rc.1+build.1")),
+    ?assertMatch(true, ec_semver:lte("1.0.0-rc.1+build.1",
+                           "1.0.0")),
+    ?assertMatch(true, ec_semver:lte("1.0.0",
+                           "1.0.0+0.3.7")),
+    ?assertMatch(true, ec_semver:lte("1.0.0+0.3.7",
+                           "1.3.7+build")),
+    ?assertMatch(true, ec_semver:lte("1.3.7+build",
+                           "1.3.7+build.2.b8f12d7")),
+    ?assertMatch(true, ec_semver:lte("1.3.7+build.2.b8f12d7",
+                           "1.3.7+build.11.e0f985a")),
+    ?assertMatch(true, ec_semver:lte("1.0.0-alpha",
+                           "1.0.0-alpha")),
+    ?assertMatch(true, ec_semver:lte("1",
+                           "1.0.0")),
+    ?assertMatch(true, ec_semver:lte("1.0",
+                           "1.0.0")),
+    ?assertMatch(true, ec_semver:lte("1.0.0",
+                           "1")),
+    ?assertMatch(true, ec_semver:lte("1.0+alpha.1",
+                           "1.0.0+alpha.1")),
+    ?assertMatch(true, ec_semver:lte("1.0.0.0+alpha.1",
+                           "1.0.0+alpha.1")),
+    ?assertMatch(true, ec_semver:lte("1.0-alpha.1+build.1",
+                           "1.0.0-alpha.1+build.1")),
+    ?assertMatch(true, ec_semver:lte("aa","cc")),
+    ?assertMatch(true, ec_semver:lte("cc","cc")),
+    ?assertMatch(true, not ec_semver:lte("1.0.0-alpha.1",
+                              "1.0.0-alpha")),
+    ?assertMatch(true, not ec_semver:lte("1.0.0-pre-alpha.2",
+                               "1.0.0-pre-alpha")),
+    ?assertMatch(true, not ec_semver:lte("cc", "aa")),
+    ?assertMatch(true, not ec_semver:lte("1.0.0-beta.2",
+                              "1.0.0-alpha.1")),
+    ?assertMatch(true, not ec_semver:lte("1.0.0-beta.11",
+                              "1.0.0-beta.2")),
+    ?assertMatch(true, not ec_semver:lte("1.0.0-rc.1", "1.0.0-beta.11")),
+    ?assertMatch(true, not ec_semver:lte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
+    ?assertMatch(true, not ec_semver:lte("1.0.0", "1.0.0-rc.1+build.1")),
+    ?assertMatch(true, not ec_semver:lte("1.0.0+0.3.7", "1.0.0")),
+    ?assertMatch(true, not ec_semver:lte("1.3.7+build", "1.0.0+0.3.7")),
+    ?assertMatch(true, not ec_semver:lte("1.3.7+build.2.b8f12d7",
+                              "1.3.7+build")),
+    ?assertMatch(true, not ec_semver:lte("1.3.7+build.11.e0f985a",
+                              "1.3.7+build.2.b8f12d7")).
+
+between_test() ->
+    ?assertMatch(true, ec_semver:between("1.0.0-alpha",
+                               "1.0.0-alpha.3",
+                               "1.0.0-alpha.2")),
+    ?assertMatch(true, ec_semver:between("1.0.0-alpha.1",
+                               "1.0.0-beta.2",
+                               "1.0.0-alpha.25")),
+    ?assertMatch(true, ec_semver:between("1.0.0-beta.2",
+                               "1.0.0-beta.11",
+                               "1.0.0-beta.7")),
+    ?assertMatch(true, ec_semver:between("1.0.0-pre-alpha.2",
+                               "1.0.0-pre-alpha.11",
+                               "1.0.0-pre-alpha.7")),
+    ?assertMatch(true, ec_semver:between("1.0.0-beta.11",
+                               "1.0.0-rc.3",
+                               "1.0.0-rc.1")),
+    ?assertMatch(true, ec_semver:between("1.0.0-rc.1",
+                               "1.0.0-rc.1+build.3",
+                               "1.0.0-rc.1+build.1")),
+
+    ?assertMatch(true, ec_semver:between("1.0.0.0-rc.1",
+                               "1.0.0-rc.1+build.3",
+                               "1.0.0-rc.1+build.1")),
+    ?assertMatch(true, ec_semver:between("1.0.0-rc.1+build.1",
+                               "1.0.0",
+                               "1.0.0-rc.33")),
+    ?assertMatch(true, ec_semver:between("1.0.0",
+                               "1.0.0+0.3.7",
+                               "1.0.0+0.2")),
+    ?assertMatch(true, ec_semver:between("1.0.0+0.3.7",
+                               "1.3.7+build",
+                               "1.2")),
+    ?assertMatch(true, ec_semver:between("1.3.7+build",
+                               "1.3.7+build.2.b8f12d7",
+                               "1.3.7+build.1")),
+    ?assertMatch(true, ec_semver:between("1.3.7+build.2.b8f12d7",
+                               "1.3.7+build.11.e0f985a",
+                               "1.3.7+build.10.a36faa")),
+    ?assertMatch(true, ec_semver:between("1.0.0-alpha",
+                               "1.0.0-alpha",
+                               "1.0.0-alpha")),
+    ?assertMatch(true, ec_semver:between("1",
+                               "1.0.0",
+                               "1.0.0")),
+    ?assertMatch(true, ec_semver:between("1.0",
+                               "1.0.0",
+                               "1.0.0")),
+
+    ?assertMatch(true, ec_semver:between("1.0",
+                               "1.0.0.0",
+                               "1.0.0.0")),
+    ?assertMatch(true, ec_semver:between("1.0.0",
+                               "1",
+                               "1")),
+    ?assertMatch(true, ec_semver:between("1.0+alpha.1",
+                               "1.0.0+alpha.1",
+                               "1.0.0+alpha.1")),
+    ?assertMatch(true, ec_semver:between("1.0-alpha.1+build.1",
+                               "1.0.0-alpha.1+build.1",
+                               "1.0.0-alpha.1+build.1")),
+    ?assertMatch(true, ec_semver:between("aaa",
+                               "ddd",
+                               "cc")),
+    ?assertMatch(true, not ec_semver:between("1.0.0-alpha.1",
+                                   "1.0.0-alpha.22",
+                                   "1.0.0")),
+    ?assertMatch(true, not ec_semver:between("1.0.0-pre-alpha.1",
+                                   "1.0.0-pre-alpha.22",
+                                   "1.0.0")),
+    ?assertMatch(true, not ec_semver:between("1.0.0",
+                                   "1.0.0-alpha.1",
+                                   "2.0")),
+    ?assertMatch(true, not ec_semver:between("1.0.0-beta.1",
+                                   "1.0.0-beta.11",
+                                   "1.0.0-alpha")),
+    ?assertMatch(true, not ec_semver:between("1.0.0-beta.11", "1.0.0-rc.1",
+                                   "1.0.0-rc.22")),
+    ?assertMatch(true, not ec_semver:between("aaa", "ddd", "zzz")).
+
+pes_test() ->
+    ?assertMatch(true, ec_semver:pes("1.0.0-rc.0", "1.0.0-rc.0")),
+    ?assertMatch(true, ec_semver:pes("1.0.0-rc.1", "1.0.0-rc.0")),
+    ?assertMatch(true, ec_semver:pes("1.0.0", "1.0.0-rc.0")),
+    ?assertMatch(false, ec_semver:pes("1.0.0-rc.0", "1.0.0-rc.1")),
+    ?assertMatch(true, ec_semver:pes("2.6.0", "2.6")),
+    ?assertMatch(true, ec_semver:pes("2.7", "2.6")),
+    ?assertMatch(true, ec_semver:pes("2.8", "2.6")),
+    ?assertMatch(true, ec_semver:pes("2.9", "2.6")),
+    ?assertMatch(true, ec_semver:pes("A.B", "A.A")),
+    ?assertMatch(true, not ec_semver:pes("3.0.0", "2.6")),
+    ?assertMatch(true, not ec_semver:pes("2.5", "2.6")),
+    ?assertMatch(true, ec_semver:pes("2.6.5", "2.6.5")),
+    ?assertMatch(true, ec_semver:pes("2.6.6", "2.6.5")),
+    ?assertMatch(true, ec_semver:pes("2.6.7", "2.6.5")),
+    ?assertMatch(true, ec_semver:pes("2.6.8", "2.6.5")),
+    ?assertMatch(true, ec_semver:pes("2.6.9", "2.6.5")),
+    ?assertMatch(true, ec_semver:pes("2.6.0.9", "2.6.0.5")),
+    ?assertMatch(true, not ec_semver:pes("2.7", "2.6.5")),
+    ?assertMatch(true, not ec_semver:pes("2.1.7", "2.1.6.5")),
+    ?assertMatch(true, not ec_semver:pes("A.A", "A.B")),
+    ?assertMatch(true, not ec_semver:pes("2.5", "2.6.5")).
+
+parse_test() ->
+    ?assertEqual({1, {[],[]}}, ec_semver:parse(<<"1">>)),
+    ?assertEqual({{1,2,34},{[],[]}}, ec_semver:parse(<<"1.2.34">>)),
+    ?assertEqual({<<"a">>, {[],[]}}, ec_semver:parse(<<"a">>)),
+    ?assertEqual({{<<"a">>,<<"b">>}, {[],[]}}, ec_semver:parse(<<"a.b">>)),
+    ?assertEqual({1, {[],[]}}, ec_semver:parse(<<"1">>)),
+    ?assertEqual({{1,2}, {[],[]}}, ec_semver:parse(<<"1.2">>)),
+    ?assertEqual({{1,2,2}, {[],[]}}, ec_semver:parse(<<"1.2.2">>)),
+    ?assertEqual({{1,99,2}, {[],[]}}, ec_semver:parse(<<"1.99.2">>)),
+    ?assertEqual({{1,99,2}, {[<<"alpha">>],[]}}, ec_semver:parse(<<"1.99.2-alpha">>)),
+    ?assertEqual({{1,99,2}, {[<<"alpha">>,1], []}}, ec_semver:parse(<<"1.99.2-alpha.1">>)),
+    ?assertEqual({{1,99,2}, {[<<"pre-alpha">>,1], []}}, ec_semver:parse(<<"1.99.2-pre-alpha.1">>)),
+    ?assertEqual({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}},
+        ec_semver:parse(<<"1.99.2+build.1.a36">>)),
+    ?assertEqual({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}},
+        ec_semver:parse(<<"1.99.2.44+build.1.a36">>)),
+    ?assertEqual({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}},
+        ec_semver:parse("1.99.2-alpha.1+build.1.a36")),
+    ?assertEqual({{1,99,2}, {[<<"pre-alpha">>, 1], [<<"build">>, 1, <<"a36">>]}},
+        ec_semver:parse("1.99.2-pre-alpha.1+build.1.a36")).
+
+version_format_test() ->
+    ?assertEqual(["1", [], []], ec_semver:format({1, {[],[]}})),
+    ?assertEqual(["1", ".", "2", ".", "34", [], []], ec_semver:format({{1,2,34},{[],[]}})),
+    ?assertEqual(<<"a">>, erlang:iolist_to_binary(ec_semver:format({<<"a">>, {[],[]}}))),
+    ?assertEqual(<<"a.b">>, erlang:iolist_to_binary(ec_semver:format({{<<"a">>,<<"b">>}, {[],[]}}))),
+    ?assertEqual(<<"1">>, erlang:iolist_to_binary(ec_semver:format({1, {[],[]}}))),
+    ?assertEqual(<<"1.2">>, erlang:iolist_to_binary(ec_semver:format({{1,2}, {[],[]}}))),
+    ?assertEqual(<<"1.2.2">>, erlang:iolist_to_binary(ec_semver:format({{1,2,2}, {[],[]}}))),
+    ?assertEqual(<<"1.99.2">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[],[]}}))),
+    ?assertEqual(<<"1.99.2-alpha">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"alpha">>],[]}}))),
+    ?assertEqual(<<"1.99.2-alpha.1">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"alpha">>,1], []}}))),
+    ?assertEqual(<<"1.99.2-pre-alpha.1">>, erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"pre-alpha">>,1], []}}))),
+    ?assertEqual(<<"1.99.2+build.1.a36">>,
+                 erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}}))),
+    ?assertEqual(<<"1.99.2.44+build.1.a36">>,
+                 erlang:iolist_to_binary(ec_semver:format({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}}))),
+    ?assertEqual(<<"1.99.2-alpha.1+build.1.a36">>,
+                 erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
+    ?assertEqual(<<"1.99.2-pre-alpha.1+build.1.a36">>,
+        erlang:iolist_to_binary(ec_semver:format({{1,99,2}, {[<<"pre-alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
+    ?assertEqual(<<"1">>, erlang:iolist_to_binary(ec_semver:format({1, {[],[]}}))).
diff --git a/test/ec_talk_tests.erl b/test/ec_talk_tests.erl
new file mode 100644
index 0000000..9b7bd07
--- /dev/null
+++ b/test/ec_talk_tests.erl
@@ -0,0 +1,19 @@
+%%% @copyright 2024 Erlware, LLC.
+-module(ec_talk_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+general_test_() ->
+    [?_test(42 == ec_talk:get_integer("42")),
+     ?_test(500_211 == ec_talk:get_integer("500211")),
+     ?_test(1_234_567_890 == ec_talk:get_integer("1234567890")),
+     ?_test(12_345_678_901_234_567_890 == ec_talk:get_integer("12345678901234567890")),
+     ?_test(true == ec_talk:get_boolean("true")),
+     ?_test(false == ec_talk:get_boolean("false")),
+     ?_test(true == ec_talk:get_boolean("Ok")),
+     ?_test(true == ec_talk:get_boolean("ok")),
+     ?_test(true == ec_talk:get_boolean("Y")),
+     ?_test(true == ec_talk:get_boolean("y")),
+     ?_test(false == ec_talk:get_boolean("False")),
+     ?_test(false == ec_talk:get_boolean("No")),
+     ?_test(false == ec_talk:get_boolean("no"))].