Compare commits

..

No commits in common. "master" and "v0.10.1" have entirely different histories.

45 changed files with 1920 additions and 2704 deletions

View file

@ -1,31 +0,0 @@
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

7
.gitignore vendored
View file

@ -7,12 +7,7 @@ doc/edoc-info
doc/erlang.png
ebin/*
.*
!.github
_build
erl_crash.dump
*.pyc
*~
TEST-*.xml
/foo
src/ec_semver_parser.peg
src/ec_semver_parser.erl

15
.travis.yml Normal file
View file

@ -0,0 +1,15 @@
language: erlang
otp_release:
- R16B
- R15B02
- R15B01
- R15B
- R14B04
- R14B03
- R14B02
before_script: "make get-deps"
script: "make"
branches:
only:
- master
- next

View file

@ -1,139 +0,0 @@
Contributing
============
Introduction
------------
This document describes the usages and rules to follow when contributing
to this project.
It uses the uppercase keywords SHOULD for optional but highly recommended
conditions and MUST for required conditions.
`git` is a distributed source code versioning system. This document refers
to three different repositories hosting the source code of the project.
`Your local copy` refers to the copy of the repository that you have on
your computer. The remote repository `origin` refers to your fork of the
project's repository that you can find in your GitHub account. The remote
repository `upstream` refers to the official repository for this project.
Reporting bugs
--------------
Upon identifying a bug you SHOULD submit a ticket, regardless of your
plan for fixing it. If you plan to fix the bug, you SHOULD discuss your
plans to avoid having your work rejected.
Before implementing a new feature, you SHOULD submit a ticket for discussion
on your plans. The feature might have been rejected already, or the
implementation might already be decided.
Cloning
-------
You MUST fork the project's repository to your GitHub account by clicking
on the `Fork` button.
Then, from your fork's page, copy the `Git Read-Only` URL to your clipboard.
You MUST perform the following commands in the folder you choose, replacing
`$URL` by the URL you just copied, `$UPSTREAM_URL` by the `Git Read-Only`
project of the official repository, and `$PROJECT` by the name of this project.
``` bash
$ git clone "$URL"
$ cd $PROJECT
$ git remote add upstream $UPSTREAM_URL
```
Branching
---------
Before starting working on the code, you MUST update to `upstream`. The
project is always evolving, and as such you SHOULD always strive to keep
up to date when submitting patches to make sure they can be merged without
conflicts.
To update the current branch to `upstream`, you can use the following commands.
``` bash
$ git fetch upstream
$ git rebase upstream/master
```
It may ask you to stash your changes, in which case you stash with:
``` bash
$ git stash
```
And put your changes back in with:
``` bash
$ 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 responsibility
to deal with them.
You MUST create a new branch for your work. First make sure you have
'fetched' `master`
``` bash
$ git checkout -b $BRANCH upstream/master
```
You MUST use a an insightful branch name.
If you later need to switch back to an existing branch `$BRANCH`, you can use:
``` bash
$ git checkout $BRANCH
```
Source editing
--------------
The following rules MUST be followed:
* Indentation uses 4 horizontal spaces
* Tabs should not be used
* Do NOT align code; only indentation is allowed
The following rules SHOULD be followed:
* Write small functions whenever possible
* Avoid having too many clauses containing clauses containing clauses
* Lines SHOULD NOT span more than 80 columns
When in doubt indentation as performed in the Erlang Emacs Mode is
correct.
Committing
----------
You MUST ensure that all commits pass all tests and do not have extra
Dialyzer warnings.
You MUST put all the related work in a single commit. Fixing a bug is one
commit, adding a feature is one commit, adding two features is two commits.
You MUST write a proper commit title and message. The commit title MUST be
at most 72 characters; it is the first line of the commit text. The second
line of the commit text MUST be left blank. The third line and beyond is the
commit message. You SHOULD write a commit message. If you do, you MUST make
all lines smaller than 80 characters. You SHOULD explain what the commit
does, what references you used and any other information that helps
understanding your work.
Submitting the pull request
---------------------------
You MUST push your branch `$BRANCH` to GitHub, using the following command:
``` bash
$ git push origin $BRANCH
```
You MUST then submit the pull request by using the GitHub interface to
the `master` branch. You SHOULD provide an explanatory message and refer
to any previous ticket related to this patch.

86
Makefile Normal file
View file

@ -0,0 +1,86 @@
# Copyright 2012 Erlware, LLC. All Rights Reserved.
#
# BSD License see COPYING
ERL = $(shell which erl)
ERL_VER = $(shell erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell)
ERLFLAGS= -pa $(CURDIR)/.eunit -pa $(CURDIR)/ebin -pa $(CURDIR)/*/ebin
REBAR=$(shell which rebar)
ifeq ($(REBAR),)
$(error "Rebar not available on this system")
endif
ERLWARE_COMMONS_PLT=$(CURDIR)/.erlware_commons_plt
.PHONY: all compile doc clean test shell distclean pdf get-deps rebuild #dialyzer typer #fail on Travis.
all: compile doc test #dialyzer #fail on travis
get-deps:
$(REBAR) get-deps
$(REBAR) compile
compile:
$(REBAR) skip_deps=true compile
doc: compile
- $(REBAR) skip_deps=true doc
test: compile
$(REBAR) skip_deps=true eunit
$(ERLWARE_COMMONS_PLT).$(ERL_VER).erts:
@echo Building local plt at $(ERLWARE_COMMONS_PLT).$(ERL_VER).base
@echo
- dialyzer --fullpath --verbose --output_plt $(ERLWARE_COMMONS_PLT).$(ERL_VER).base --build_plt \
--apps erts
$(ERLWARE_COMMONS_PLT).$(ERL_VER).kernel:$(ERLWARE_COMMONS_PLT).$(ERL_VER).erts
@echo Building local plt at $(ERLWARE_COMMONS_PLT).$(ERL_VER).base
@echo
- dialyzer --fullpath --verbose --output_plt $(ERLWARE_COMMONS_PLT).$(ERL_VER).base --build_plt \
--apps kernel
$(ERLWARE_COMMONS_PLT).$(ERL_VER).base:$(ERLWARE_COMMONS_PLT).$(ERL_VER).kernel
@echo Building local plt at $(ERLWARE_COMMONS_PLT).$(ERL_VER).base
@echo
- dialyzer --fullpath --verbose --output_plt $(ERLWARE_COMMONS_PLT).$(ERL_VER).base --build_plt \
--apps stdlib
$(ERLWARE_COMMONS_PLT).$(ERL_VER): $(ERLWARE_COMMONS_PLT).$(ERL_VER).base
@echo Building local plt at $(ERLWARE_COMMONS_PLT).$(ERL_VER)
@echo
- dialyzer --fullpath --verbose --output_plt $(ERLWARE_COMMONS_PLT).$(ERL_VER) --add_to_plt --plt $(ERLWARE_COMMONS_PLT).$(ERL_VER).base \
--apps eunit -r deps
dialyzer: $(ERLWARE_COMMONS_PLT).$(ERL_VER)
dialyzer --fullpath --plt $(ERLWARE_COMMONS_PLT).$(ERL_VER) -Wrace_conditions -r ./ebin
typer: $(ERLWARE_COMMONS_PLT).$(ERL)VER(
typer --plt $(ERLWARE_COMMONS_PLT).$(ERL_VER) -r ./src
shell: compile
# You often want *rebuilt* rebar tests to be available to the
# shell you have to call eunit (to get the tests
# rebuilt). However, eunit runs the tests, which probably
# fails (thats probably why You want them in the shell). This
# runs eunit but tells make to ignore the result.
- @$(REBAR) skip_deps=true eunit
@$(ERL) $(ERLFLAGS)
clean:
$(REBAR) skip_deps=true clean
- rm $(CURDIR)/doc/*.html
- rm $(CURDIR)/doc/*.css
- rm $(CURDIR)/doc/*.png
- rm $(CURDIR)/doc/edoc-info
distclean: clean
rm -rf $(ERLWARE_COMMONS_PLT).$(ERL_VER)
rm -rvf $(CURDIR)/deps/*
rebuild: distclean get-deps all

View file

@ -3,9 +3,7 @@ Erlware Commons
Current Status
--------------
[![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)
[![Build Status](https://secure.travis-ci.org/erlware/erlware_commons.png)](http://travis-ci.org/erlware/erlware_commons)
Introduction
------------
@ -26,18 +24,6 @@ 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
------------------------------------
@ -70,7 +56,7 @@ href="http://www.erlang.org/doc/man/lists.html">lists</a>, 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 parallelizing inside a cluster. It handles errors and node
nodes, for parallizing inside a cluster. It handles errors and node
failures. It can be configured, tuned, and tweaked to get optimal
performance while minimizing overhead.
@ -78,7 +64,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
parallelize the operation.
parallize the operation.
fold is the one exception, parallel fold is different from linear
fold. This module also include a simple mapreduce implementation, and
@ -90,14 +76,14 @@ 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/erlware/erlware_commons/blob/master/src/ec_lists.erl)
### [ec_lists](https://github.com/ericbmerritt/erlware_commons/blob/master/src/ec_lists.erl)
A set of additional list manipulation functions designed to supliment
the `lists` module in stdlib.
### [ec_talk](https://github.com/erlware/erlware_commons/blob/master/src/ec_talk.erl)
A set of simple utility functions to facilitate command line
A set of simple utility functions to falicitate command line
communication with a user.
Signatures
@ -107,7 +93,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 mechanism called *signatures* and several modules that
have defined a mechnism 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).
@ -124,19 +110,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/erlware/erlware_commons/blob/master/src/ec_gb_trees.erl)
### [ec_gb_trees](https://github.com/ericbmerritt/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/erlware/erlware_commons/blob/master/src/ec_orddict.erl)
### [ec_orddict](https://github.com/ericbmerritt/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/erlware/erlware_commons/blob/master/src/ec_rbdict.erl)
### [ec_rbdict](https://github.com/ericbmerritt/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

View file

@ -0,0 +1,363 @@
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.

View file

@ -2,10 +2,10 @@ Signatures
==========
It often occurs in coding that we need a library, a set of
functionalities. Often there are several algorithms that could provide
each of these functionalities. However, the code that uses it, either doesn't
functionaly. Often there are several algorithms that could provide
this functionality. 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. Let's take the concrete example of
that algorithm to some higher level. Lets 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 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)$
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)
comparisons for lookup. While for skip lists the performance
characteristics are about as good as that of randomly-built binary
search trees - namely $\mathcal{O}(\log n)$. So the choice of which to select
search trees - namely (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 that's so easy to do at the moment.
good idea. Unfortunately, in Erlang thats ot 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,20 +39,17 @@ 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
:::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
:::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
@ -66,22 +63,21 @@ 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 inadvertently pass an unexpected function name as
someone could inadvertantly 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 flexible language so we can use a
Fortunately, Erlang is a pretty flexable language so we can use a
similar approach with a few adjustments to give us the best of both
worlds. Both the flexibility of ignoring a specific implementation
worlds. Both the flexibiltiy of ignoreing 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
:::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
@ -94,14 +90,14 @@ 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 have built in?
How do we actually do this in Erlang now that Erlang is missing what Java, SML and friends has 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
:::erlang
-module(ec_dictionary).
-export([behaviour_info/1]).
@ -119,17 +115,16 @@ behaviour_info(callbacks) ->
{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 won't help us actually use the
the functions they need to have, but it wont help use 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
:::erlang
%% @doc create a new dictionary object from the specified module. The
%% module should implement the dictionary behaviour.
%%
@ -137,13 +132,12 @@ behaviour, starting with `new/1`.
-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 in the record. We call the module name named in
data. here 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
@ -154,7 +148,7 @@ 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
:::erlang
%% @doc add a new value to the existing dictionary. Return a new
%% dictionary containing the value.
%%
@ -164,7 +158,6 @@ adding a value.
-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.
@ -180,7 +173,7 @@ implementation to do the work itself.
Now lets do a data retrieval function. In this case, the `get` function
of the dictionary Signature.
```erlang
:::erlang
%% @doc given a key return that key from the dictionary. If the key is
%% not found throw a 'not_found' exception.
%%
@ -190,7 +183,6 @@ of the dictionary Signature.
-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
@ -205,7 +197,7 @@ implementation in
Using Signatures
----------------
It's a good idea to work through an example so we have a bit better
Its 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
@ -214,7 +206,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 implementation, the second is a
Signatures. The first is a native implementations, the second is a
wrapper.
### Native Signature Implementations
@ -231,20 +223,20 @@ 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 of this in our current example
defined by a Signature. A good example if 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. Let's take a look at one example to see how this is done.
itself. Lets 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 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.
`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.
```erlang
:::erlang
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
ec_dictionary:value(V).
get(Key, Data) ->
@ -254,10 +246,9 @@ get(Key, Data) ->
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.
@ -267,25 +258,24 @@ the mix and that adds a bit of additional overhead.
### Creating the Timing Module
We are going to be creating timings for both Native Signature
We are going to creating timings for both Native Signature
Implementations and Signature Wrappers.
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
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
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
:::erlang
create_dict() ->
lists:foldl(fun(El, Dict) ->
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.
@ -293,27 +283,26 @@ of the dictionaries themselves.
We need to create a similar function for our Signature based
dictionary `ec_dict`.
```erlang
:::erlang
create_dictionary(Type) ->
lists:foldl(fun(El, Dict) ->
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 dictionary
given by `create_dict/0`.
Signatures. Otherwise, this does the exact same thing as the dict
`create_dict/1`.
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 we are going to use the `size` function as well as
dictionaries that we are going to use the `size` function as well as
the `add` function.
```erlang
:::erlang
time_direct_vs_signature_dict() ->
io:format("Timing dict~n"),
Dict = create_dict(),
@ -323,19 +312,18 @@ time_direct_vs_signature_dict() ->
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 (it's
fast so it doesn't take long). You can see in the anonymous
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
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
:::erlang
time_dict_type(Type) ->
io:format("Testing ~p~n", [Type]),
Dict = create_dictionary(Type),
@ -343,11 +331,10 @@ time_dict_type(Type) ->
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.
@ -356,7 +343,7 @@ 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
:::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)
@ -372,10 +359,9 @@ 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. That's 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. Thats 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
@ -387,22 +373,21 @@ 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
:::erlang
create_rbdict() ->
lists:foldl(fun(El, Dict) ->
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
:::erlang
time_direct_vs_signature_rbdict() ->
io:format("Timing rbdict~n"),
Dict = create_rbdict(),
@ -412,7 +397,6 @@ time_direct_vs_signature_rbdict() ->
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?
@ -422,7 +406,7 @@ 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
:::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)
@ -438,19 +422,18 @@ 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 it's really, really small - or small
difference is. Essentially that means its 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. They have little or no overhead depending on the type of
in Erlang. The have little or no over head depending on the type of
implementation, and greatly increase the flexibility of the a library
while retaining testability and locality.

View file

@ -1,24 +0,0 @@
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
%%% Version 2.0 (the "License"); you may not use this file
%%% except in compliance with the License. You may obtain
%%% a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing,
%%% software distributed under the License is distributed on an
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%%% KIND, either express or implied. See the License for the
%%% specific language governing permissions and limitations
%%% under the License.
%%%---------------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright (C) 2012 Erlware, LLC.
-define(EC_ERROR, 0).
-define(EC_WARN, 1).
-define(EC_INFO, 2).
-define(EC_DEBUG, 3).

View file

@ -1,14 +1,18 @@
%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
%% Dependencies ================================================================
{deps, [
{cf, "~>0.3"}
]}.
{deps, [{neotoma, "",
{git, "https://github.com/seancribbs/neotoma.git", {branch, master}}},
{proper, "", {git, "https://github.com/bkearns/proper.git", {branch, master}}},
{rebar_vsn_plugin, ".*", {git, "https://github.com/erlware/rebar_vsn_plugin.git",
{branch, "master"}}}]}.
{erl_first_files, ["ec_dictionary", "ec_vsn"]}.
{erl_first_files, ["ec_dictionary"]}.
%% Compiler Options ============================================================
{erl_opts, [debug_info, warnings_as_errors]}.
{erl_opts,
[debug_info,
warnings_as_errors]}.
%% EUnit =======================================================================
{eunit_opts, [verbose,
@ -17,8 +21,5 @@
{cover_enabled, true}.
{cover_print_enabled, true}.
%% Profiles ====================================================================
{profiles, [{dev, [{deps,
[{neotoma, "",
{git, "https://github.com/seancribbs/neotoma.git", {branch, master}}}]}]}
]}.
%% Rebar Plugins ==============================================================
{plugins, [rebar_vsn_plugin]}.

View file

@ -1,7 +1,15 @@
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.
{match, [ErtsNumber]} = re:run(erlang:system_info(otp_release), "R(\\d+).+", [{capture, [1], list}]),
ErtsVsn = erlang:list_to_integer(ErtsNumber),
Opts1 = case lists:keysearch(erl_opts, 1, CONFIG) of
{value, {erl_opts, Opts0}} ->
Opts0;
false ->
[]
end,
Opts2 = if
ErtsVsn >= 15 ->
[{d, have_callback_support} | Opts1];
true ->
Opts1
end,
lists:keystore(erl_opts, 1, CONFIG, {erl_opts, Opts2}).

View file

@ -1,8 +0,0 @@
{"1.2.0",
[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},0}]}.
[
{pkg_hash,[
{<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>}]},
{pkg_hash_ext,[
{<<"cf">>, <<"315E8D447D3A4B02BCDBFA397AD03BBB988A6E0AA6F44D3ADD0F4E3C3BF97672">>}]}
].

BIN
rebar3

Binary file not shown.

View file

@ -1,4 +1,3 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.

View file

@ -1,257 +0,0 @@
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
%%% Version 2.0 (the "License"); you may not use this file
%%% except in compliance with the License. You may obtain
%%% a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing,
%%% software distributed under the License is distributed on an
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%%% KIND, either express or implied. See the License for the
%%% specific language governing permissions and limitations
%%% under the License.
%%%---------------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @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 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,
debug/3,
info/2,
info/3,
error/2,
error/3,
warn/2,
warn/3,
log_level/1,
atom_log_level/1,
colorize/4,
format/1]).
-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(),
intensity=low :: intensity()}).
%%============================================================================
%% types
%%============================================================================
-export_type([t/0,
int_log_level/0,
atom_log_level/0,
log_level/0,
caller/0,
log_fun/0]).
-type caller() :: api | command_line.
-type log_level() :: int_log_level() | atom_log_level().
-type int_log_level() :: 0..3.
-type atom_log_level() :: error | warn | info | debug.
-type intensity() :: none | low | high.
-type log_fun() :: fun(() -> iolist()).
-type color() :: char().
-opaque t() :: #state_t{}.
%%============================================================================
%% API
%%============================================================================
%% @doc Create a new 'log level' for the system
-spec new(log_level()) -> t().
new(LogLevel) ->
new(LogLevel, api).
-spec new(log_level(), caller()) -> t().
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;
AtomLogLevel =:= debug ->
LogLevel = case AtomLogLevel of
error -> 0;
warn -> 1;
info -> 2;
debug -> 3
end,
new(LogLevel, Caller, Intensity).
%% @doc log at the debug level given the current log state with a string or
%% function that returns a string
-spec debug(t(), string() | log_fun()) -> ok.
debug(LogState, Fun)
when erlang:is_function(Fun) ->
log(LogState, ?EC_DEBUG, fun() ->
colorize(LogState, ?CYAN, false, Fun())
end);
debug(LogState, String) ->
debug(LogState, "~ts~n", [String]).
%% @doc log at the debug level given the current log state with a format string
%% 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).
%% @doc log at the info level given the current log state with a string or
%% function that returns a string
-spec info(t(), string() | log_fun()) -> ok.
info(LogState, Fun)
when erlang:is_function(Fun) ->
log(LogState, ?EC_INFO, fun() ->
colorize(LogState, ?GREEN, false, Fun())
end);
info(LogState, String) ->
info(LogState, "~ts~n", [String]).
%% @doc log at the info level given the current log state with a format string
%% 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).
%% @doc log at the error level given the current log state with a string or
%% format string that returns a function
-spec error(t(), string() | log_fun()) -> ok.
error(LogState, Fun)
when erlang:is_function(Fun) ->
log(LogState, ?EC_ERROR, fun() ->
colorize(LogState, ?RED, false, Fun())
end);
error(LogState, String) ->
error(LogState, "~ts~n", [String]).
%% @doc log at the error level given the current log state with a format string
%% 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).
%% @doc log at the warn level given the current log state with a string or
%% format string that returns a function
-spec warn(t(), string() | log_fun()) -> ok.
warn(LogState, Fun)
when erlang:is_function(Fun) ->
log(LogState, ?EC_WARN, fun() -> colorize(LogState, ?MAGENTA, false, Fun()) end);
warn(LogState, String) ->
warn(LogState, "~ts~n", [String]).
%% @doc log at the warn level given the current log state with a format string
%% 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).
%% @doc Execute the fun passed in if log level is as expected.
-spec log(t(), int_log_level(), log_fun()) -> ok.
log(#state_t{log_level=DetailLogLevel}, LogLevel, Fun)
when DetailLogLevel >= LogLevel ->
io:format("~ts~n", [Fun()]);
log(_, _, _) ->
ok.
%% @doc when the module log level is less then or equal to the log level for the
%% call then write the log info out. When its not then ignore the call.
-spec log(t(), int_log_level(), string(), [any()]) -> ok.
log(#state_t{log_level=DetailLogLevel}, LogLevel, FormatString, Args)
when DetailLogLevel >= LogLevel,
erlang:is_list(Args) ->
io:format(FormatString, Args);
log(_, _, _, _) ->
ok.
%% @doc return a boolean indicating if the system should log for the specified
%% levelg
-spec should(t(), int_log_level() | any()) -> boolean().
should(#state_t{log_level=DetailLogLevel}, LogLevel)
when DetailLogLevel >= LogLevel ->
true;
should(_, _) ->
false.
%% @doc get the current log level as an integer
-spec log_level(t()) -> int_log_level().
log_level(#state_t{log_level=DetailLogLevel}) ->
DetailLogLevel.
%% @doc get the current log level as an atom
-spec atom_log_level(t()) -> atom_log_level().
atom_log_level(#state_t{log_level=?EC_ERROR}) ->
error;
atom_log_level(#state_t{log_level=?EC_WARN}) ->
warn;
atom_log_level(#state_t{log_level=?EC_INFO}) ->
info;
atom_log_level(#state_t{log_level=?EC_DEBUG}) ->
debug.
-spec format(t()) -> iolist().
format(Log) ->
[<<"(">>,
ec_cnv:to_binary(log_level(Log)), <<":">>,
ec_cnv:to_binary(atom_log_level(Log)),
<<")">>].
-spec colorize(t(), color(), boolean(), string()) -> string().
-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.

View file

@ -1,7 +0,0 @@
%%% @copyright 2024 Erlware, LLC.
-define(RED, $r).
-define(GREEN, $g).
-define(YELLOW, $y).
-define(BLUE, $b).
-define(MAGENTA, $m).
-define(CYAN, $c).

View file

@ -1,214 +0,0 @@
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the Apache License,
%%% Version 2.0 (the "License"); you may not use this file
%%% except in compliance with the License. You may obtain
%%% a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing,
%%% software distributed under the License is distributed on an
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%%% KIND, either express or implied. See the License for the
%%% specific language governing permissions and limitations
%%% under the License.
%%%---------------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright (C) 2012 Erlware, LLC.
%%%
-module(ec_cnv).
%% API
-export([to_integer/1,
to_integer/2,
to_float/1,
to_float/2,
to_number/1,
to_list/1,
to_binary/1,
to_atom/1,
to_boolean/1,
is_true/1,
is_false/1]).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc
%% Automatic conversion of a term into integer type. The conversion
%% will round a float value if nonstrict is specified otherwise badarg
-spec to_integer(string() | binary() | integer() | float()) ->
integer().
to_integer(X)->
to_integer(X, nonstrict).
-spec to_integer(string() | binary() | integer() | float(),
strict | nonstrict) ->
integer().
to_integer(X, strict)
when erlang:is_float(X) ->
erlang:error(badarg);
to_integer(X, nonstrict)
when erlang:is_float(X) ->
erlang:round(X);
to_integer(X, S)
when erlang:is_binary(X) ->
to_integer(erlang:binary_to_list(X), S);
to_integer(X, S)
when erlang:is_list(X) ->
try erlang:list_to_integer(X) of
Result ->
Result
catch
error:badarg when S =:= nonstrict ->
erlang:round(erlang:list_to_float(X))
end;
to_integer(X, _)
when erlang:is_integer(X) ->
X.
%% @doc
%% Automatic conversion of a term into float type. badarg if strict
%% is defined and an integer value is passed.
-spec to_float(string() | binary() | integer() | float()) ->
float().
to_float(X) ->
to_float(X, nonstrict).
-spec to_float(string() | binary() | integer() | float(),
strict | nonstrict) ->
float().
to_float(X, S) when is_binary(X) ->
to_float(erlang:binary_to_list(X), S);
to_float(X, S)
when erlang:is_list(X) ->
try erlang:list_to_float(X) of
Result ->
Result
catch
error:badarg when S =:= nonstrict ->
erlang:list_to_integer(X) * 1.0
end;
to_float(X, strict) when
erlang:is_integer(X) ->
erlang:error(badarg);
to_float(X, nonstrict)
when erlang:is_integer(X) ->
X * 1.0;
to_float(X, _) when erlang:is_float(X) ->
X.
%% @doc
%% Automatic conversion of a term into number type.
-spec to_number(binary() | string() | number()) ->
number().
to_number(X)
when erlang:is_number(X) ->
X;
to_number(X)
when erlang:is_binary(X) ->
to_number(to_list(X));
to_number(X)
when erlang:is_list(X) ->
try list_to_integer(X) of
Int -> Int
catch
error:badarg ->
list_to_float(X)
end.
%% @doc
%% Automatic conversion of a term into Erlang list
-spec to_list(atom() | list() | binary() | integer() | float()) ->
list().
to_list(X)
when erlang:is_float(X) ->
erlang:float_to_list(X);
to_list(X)
when erlang:is_integer(X) ->
erlang:integer_to_list(X);
to_list(X)
when erlang:is_binary(X) ->
erlang:binary_to_list(X);
to_list(X)
when erlang:is_atom(X) ->
erlang:atom_to_list(X);
to_list(X)
when erlang:is_list(X) ->
X.
%% @doc
%% Known limitations:
%% Converting [256 | _], lists with integers > 255
-spec to_binary(atom() | string() | binary() | integer() | float()) ->
binary().
to_binary(X)
when erlang:is_float(X) ->
to_binary(to_list(X));
to_binary(X)
when erlang:is_integer(X) ->
erlang:iolist_to_binary(integer_to_list(X));
to_binary(X)
when erlang:is_atom(X) ->
erlang:list_to_binary(erlang:atom_to_list(X));
to_binary(X)
when erlang:is_list(X) ->
erlang:iolist_to_binary(X);
to_binary(X)
when erlang:is_binary(X) ->
X.
-spec to_boolean(binary() | string() | atom()) ->
boolean().
to_boolean(<<"true">>) ->
true;
to_boolean("true") ->
true;
to_boolean(true) ->
true;
to_boolean(<<"false">>) ->
false;
to_boolean("false") ->
false;
to_boolean(false) ->
false.
-spec is_true(binary() | string() | atom()) ->
boolean().
is_true(<<"true">>) ->
true;
is_true("true") ->
true;
is_true(true) ->
true;
is_true(_) ->
false.
-spec is_false(binary() | string() | atom()) ->
boolean().
is_false(<<"false">>) ->
true;
is_false("false") ->
true;
is_false(false) ->
true;
is_false(_) ->
false.
%% @doc
%% Automation conversion a term to an existing atom. badarg is
%% returned if the atom doesn't exist. the safer version, won't let
%% you leak atoms
-spec to_atom(atom() | list() | binary() | integer() | float()) ->
atom().
to_atom(X)
when erlang:is_atom(X) ->
X;
to_atom(X)
when erlang:is_list(X) ->
erlang:list_to_existing_atom(X);
to_atom(X) ->
to_atom(to_list(X)).

View file

@ -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, "~ts~n", [Src]),
io:fwrite(Fd, "~s~n", [Src]),
file:close(Fd);
Error ->
Error

View file

@ -1,4 +1,3 @@
%% vi:ts=4 sw=4 et
%% @copyright Dale Harvey
%% @doc Format dates in erlang
%%
@ -25,10 +24,8 @@
-author("Dale Harvey <dale@hypernumbers.com>").
-export([format/1, format/2]).
-export([format_iso8601/1]).
-export([parse/1, parse/2]).
-export([nparse/1]).
-export([tokenise/2]).
%% These are used exclusively as guards and so the function like
%% defines make sense
@ -42,11 +39,8 @@
-define( is_day(X), (is_integer(X) andalso X =< 31) ).
-define( is_hinted_month(X), (is_tuple(X) andalso size(X)=:=2 andalso element(1,X)=:=?MONTH_TAG) ).
-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, 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").
-define(GREGORIAN_SECONDS_1970, 62167219200).
-type year() :: non_neg_integer().
-type month() :: 1..12 | {?MONTH_TAG, 1..12}.
@ -54,7 +48,7 @@
-type hour() :: 0..23.
-type minute() :: 0..59.
-type second() :: 0..59.
-type microsecond() :: 0..999_999.
-type microsecond() :: 0..1000000.
-type daynum() :: 1..7.
-type date() :: {year(),month(),day()}.
@ -79,14 +73,6 @@ format(Format, {_,_,Ms}=Now) ->
format(Format, Date) ->
format(Format, Date, []).
-spec format_iso8601(datetime()) -> string().
%% @doc format date in the ISO8601 format
%% This always puts 'Z' as time zone, since we have no notion of timezone
format_iso8601({{_, _, _}, {_, _, _}} = Date) ->
format(?ISO_8601_DATETIME_FORMAT, Date);
format_iso8601({{_, _, _}, {_, _, _, _}} = Date) ->
format(?ISO_8601_DATETIME_WITH_MS_FORMAT, Date).
-spec parse(string()) -> datetime().
%% @doc parses the datetime from a string
parse(Date) ->
@ -101,7 +87,7 @@ parse(Date, Now) ->
do_parse(Date, Now, []).
do_parse(Date, Now, Opts) ->
case filter_hints(parse(tokenise(string:uppercase(Date), []), Now, Opts)) of
case filter_hints(parse(tokenise(string:to_upper(Date), []), Now, Opts)) of
{error, bad_date} ->
erlang:throw({?MODULE, {bad_date, Date}});
{D1, T1} = {{Y, M, D}, {H, M1, S}}
@ -138,45 +124,27 @@ nparse(Date) ->
{DateS, {H, M, S, Ms} } ->
GSeconds = calendar:datetime_to_gregorian_seconds({DateS, {H, M, S} }),
ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970,
{ESeconds div 1_000_000, ESeconds rem 1_000_000, Ms};
{ESeconds div 1000000, ESeconds rem 1000000, Ms};
DateTime ->
GSeconds = calendar:datetime_to_gregorian_seconds(DateTime),
ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970,
{ESeconds div 1_000_000, ESeconds rem 1_000_000, 0}
{ESeconds div 1000000, ESeconds rem 1000000, 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}};
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}};
{{Year, Month, Day}, {hour(Hour, []), Min, Sec}, { 0}};
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 ->
@ -197,6 +165,17 @@ 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) ->
@ -208,10 +187,13 @@ 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 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) ->
%% 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}};
%% Times - 21:45, 13:45:54, 13:15PM etc
parse([Hour,$:,Min,$:,Sec | PAM], {Date, _Time}, _O) when ?is_meridian(PAM) ->
@ -305,23 +287,6 @@ 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]);
@ -421,7 +386,7 @@ 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([$+, 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.
tokenise([$+| Rest], Acc) -> tokenise(Rest, [$+ | Acc]); % 2012-12-12T12:12:12.xxxx+ ISO formatting.
tokenise([Else | Rest], Acc) ->
@ -488,11 +453,11 @@ format([$z|T], {Date,_}=Dt, Acc) ->
format(T, Dt, [itol(days_in_year(Date))|Acc]);
%% Time Formats
format([$a|T], Dt={_,{H,_,_}}, Acc) when H >= 12 ->
format([$a|T], Dt={_,{H,_,_}}, Acc) when H > 12 ->
format(T, Dt, ["pm"|Acc]);
format([$a|T], Dt={_,{_,_,_}}, Acc) ->
format(T, Dt, ["am"|Acc]);
format([$A|T], {_,{H,_,_}}=Dt, Acc) when H >= 12 ->
format([$A|T], {_,{H,_,_}}=Dt, Acc) when H > 12 ->
format(T, Dt, ["PM"|Acc]);
format([$A|T], Dt={_,{_,_,_}}, Acc) ->
format(T, Dt, ["AM"|Acc]);
@ -533,7 +498,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, [pad2(H)|Acc]);
format(T, Dt, [itol(H)|Acc]);
format([$h|T], {_,{H,_,_,_}}=Dt, Acc) when H > 12 ->
format(T, Dt, [pad2(H-12)|Acc]);
format([$h|T], {_,{H,_,_,_}}=Dt, Acc) ->
@ -545,7 +510,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, [pad6(Ms)|Acc]);
format(T, Dt, [itol(Ms)|Acc]);
%% Whole Dates
format([$c|T], {{Y,M,D},{H,Min,S}}=Dt, Acc) ->
@ -695,32 +660,26 @@ iso_week_one(Y) ->
itol(X) ->
integer_to_list(X).
-spec pad2(integer() | float()) -> list().
-spec pad2(integer()) -> 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).
%%%===================================================================
%%% Tests
%%%===================================================================
%%
%% TEST FUNCTIONS
%%
%% c(dh_date,[{d,'TEST'}]).
%-define(NOTEST, 1).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-define(DATE, {{2001,3,10},{17,16,17}}).
-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(DATEMS, {{2001,3,10},{17,16,17,123456}}).
-define(ISO, "o \\WW").
basic_format_test_() ->
@ -737,12 +696,6 @@ 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"),
?_assertEqual(format("gA",?DATE_MIDNIGHT), "12AM"),
?_assertEqual(format("h-i-s, j-m-y, it is w Day",?DATE),
"05-16-17, 10-03-01, 1631 1617 6 Satpm01"),
@ -917,12 +870,7 @@ parse_with_days_test_() ->
?_assertEqual({{2008,8,22}, {6,0,0}},
parse("Monday 22 Aug 2008 6a", ?DATE)),
?_assertEqual({{2008,8,22}, {18,35,0}},
parse("Mon, 22 Aug 2008 6:35 PM", ?DATE)),
% Twitter style
?_assertEqual({{2008,8,22}, {06,35,04}},
parse("Mon Aug 22 06:35:04 +0000 2008", ?DATE)),
?_assertEqual({{2008,8,22}, {06,35,04}},
parse("Mon Aug 22 06:35:04 +0500 2008", ?DATE))
parse("Mon, 22 Aug 2008 6:35 PM", ?DATE))
].
parse_with_TZ_test_() ->
@ -952,10 +900,9 @@ iso_test_() ->
].
ms_test_() ->
Now=os:timestamp(),
Now=now(),
[
?_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({{2012,12,12}, {12,12,12,1234}}, parse("2012-12-12T12:12:12.1234")),
?_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),
@ -966,8 +913,6 @@ 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)))
].
@ -986,97 +931,3 @@ zulu_test_() ->
?_assertEqual(format("Y-m-d\\TH:i:s",nparse("2001-03-10T15:16:17-04:00")),
"2001-03-10T19:16:17")
].
format_iso8601_test_() ->
[
?_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.

View file

@ -1,4 +1,3 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.
@ -34,7 +33,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 dictionary(_K, _V) :: dict:dict().
-type dictionary(_K, _V) :: dict().
%%%===================================================================
%%% API

View file

@ -1,4 +1,3 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.
@ -42,6 +41,8 @@
-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().
@ -53,6 +54,27 @@
-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
%%%===================================================================

View file

@ -1,4 +1,3 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
@ -11,22 +10,20 @@
exists/1,
copy/2,
copy/3,
copy_file_info/3,
insecure_mkdtemp/0,
mkdir_path/1,
mkdir_p/1,
find/2,
is_symlink/1,
is_dir/1,
type/1,
real_dir_path/1,
remove/1,
remove/2,
md5sum/1,
sha1sum/1,
read/1,
write/2,
write_term/2
write_term/2,
consult/1
]).
-export_type([
@ -41,8 +38,7 @@
%%============================================================================
%% Types
%%============================================================================
-type file_info() :: mode | time | owner | group.
-type option() :: recursive | {file_info, [file_info()]}.
-type option() :: recursive.
%%%===================================================================
%%% API
@ -59,100 +55,41 @@ 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, Options) ->
case proplists:get_value(recursive, Options, false) of
true ->
case is_dir(From) of
copy(From, To);
copy(From, To, [recursive] = Options) ->
case filelib:is_dir(From) of
false ->
copy_(From, To, Options);
copy(From, To);
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) ->
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);
case file:copy(From, To) of
{ok, _} ->
copy_file_info(To, From, proplists:get_value(file_info, Options, []));
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;
{error, Error} ->
{error, {copy_failed, Error}}
end.
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.
%% @doc return an md5 checksum string or a binary. Same as unix utility of
%% same name.
-spec md5sum(string() | binary()) -> string().
md5sum(Value) ->
bin_to_hex(crypto:hash(md5, Value)).
%% @doc return the SHA-1 digest of a string or a binary,
%% named after the UNIX utility.
-spec sha1sum(string() | binary()) -> string().
sha1sum(Value) ->
bin_to_hex(crypto:hash(sha, Value)).
bin_to_hex(Bin) ->
hex(binary_to_list(Bin)).
hex(binary_to_list(erlang:md5(Value))).
%% @doc delete a file. Use the recursive option for directories.
%% <pre>
@ -180,17 +117,8 @@ is_symlink(Path) ->
_ ->
false
end.
is_dir(Path) ->
case file:read_file_info(Path) of
{ok, #file_info{type = directory}} ->
true;
_ ->
false
end.
%% @doc returns the type of the file.
-spec type(file:name()) -> file | symlink | directory | undefined.
-spec type(file:name()) -> file | symlink | directory.
type(Path) ->
case filelib:is_regular(Path) of
true ->
@ -200,12 +128,8 @@ type(Path) ->
true ->
symlink;
false ->
case is_dir(Path) of
true -> directory;
false -> undefined
directory
end
end
end.
%% @doc gets the real path of a directory. This is mostly useful for
%% resolving symlinks. Be aware that this temporarily changes the
@ -219,11 +143,12 @@ real_dir_path(Path) ->
ok = file:set_cwd(CurCwd),
filename:absname(RealPath).
%% @doc make a unique temporary directory. Similar function to BSD stdlib
%% @doc make a unique temorory directory. Similar function to BSD stdlib
%% function of the same name.
-spec insecure_mkdtemp() -> TmpDirPath::file:name() | {error, term()}.
-spec insecure_mkdtemp() -> TmpDirPath::file:name().
insecure_mkdtemp() ->
UniqueNumber = erlang:integer_to_list(erlang:trunc(rand:uniform() * 1_000_000_000_000)),
random:seed(now()),
UniqueNumber = erlang:integer_to_list(erlang:trunc(random:uniform() * 1000000000000)),
TmpDirPath =
filename:join([tmp(), lists:flatten([".tmp_dir", UniqueNumber])]),
@ -249,8 +174,18 @@ mkdir_path(Path) ->
mkdir_p(Path).
%% @doc read a file from the file system. Provide UEX exception on failure.
-spec read(FilePath::file:filename()) -> {ok, binary()} | {error, Reason::term()}.
%% @doc consult an erlang term file from the file system.
%% Provide user readible exeption on failure.
-spec consult(FilePath::file:name()) -> term().
consult(FilePath) ->
case file:consult(FilePath) of
{ok, [Term]} ->
Term;
Error ->
Error
end.
%% @doc read a file from the file system. Provide UEX exeption on failure.
-spec read(FilePath::file:filename()) -> binary() | {error, Reason::term()}.
read(FilePath) ->
%% Now that we are moving away from exceptions again this becomes
%% a bit redundant but we want to be backwards compatible as much
@ -258,7 +193,7 @@ read(FilePath) ->
file:read_file(FilePath).
%% @doc write a file to the file system. Provide UEX exception on failure.
%% @doc write a file to the file system. Provide UEX exeption 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
@ -277,7 +212,7 @@ write_term(FileName, Term) ->
find([], _) ->
[];
find(FromDir, TargetPattern) ->
case is_dir(FromDir) of
case filelib:is_dir(FromDir) of
false ->
case re:run(FromDir, TargetPattern) of
{match, _} -> [FromDir];
@ -312,7 +247,7 @@ find_in_subdirs(FromDir, TargetPattern) ->
-spec remove_recursive(file:name(), Options::list()) -> ok | {error, Reason::term()}.
remove_recursive(Path, Options) ->
case is_dir(Path) of
case filelib:is_dir(Path) of
false ->
file:delete(Path);
true ->
@ -326,15 +261,9 @@ remove_recursive(Path, Options) ->
tmp() ->
case erlang:system_info(system_architecture) of
"win32" ->
case os:getenv("TEMP") of
false -> "./tmp";
Val -> Val
end;
"./tmp";
_SysArch ->
case os:getenv("TMPDIR") of
false -> "/tmp";
Val -> Val
end
"/tmp"
end.
%% Copy the subfiles of the From directory to the to directory.
@ -349,7 +278,7 @@ copy_subfiles(From, To, Options) ->
-spec make_dir_if_dir(file:name()) -> ok | {error, Reason::term()}.
make_dir_if_dir(File) ->
case is_dir(File) of
case filelib:is_dir(File) of
true -> ok;
false -> mkdir_path(File)
end.
@ -375,3 +304,90 @@ hex0(I) -> $0 + I.
sub_files(From) ->
{ok, SubFiles} = file:list_dir(From),
[filename:join(From, SubFile) || SubFile <- SubFiles].
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifndef(NOTEST).
-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")).
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("term", consult(TermFile)),
?assertMatch({ok, <<"\"term\". ">>}, read(TermFile)),
copy(filename:dirname(TermFile),
filename:dirname(TermFileCopy),
[recursive]),
?assertMatch("term", consult(TermFileCopy)).
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)),
?assert(lists:member(Name1, Result)),
?assert(lists:member(Name2, Result)),
?assert(lists:member(Name3, Result)),
remove(BaseDir, [recursive]).
-endif.

View file

@ -1,4 +1,3 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.
@ -26,6 +25,19 @@
from_list/1,
keys/1]).
-export_type([dictionary/2]).
%%%===================================================================
%%% Types
%%%===================================================================
%% 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 dictionary(K, V) :: {non_neg_integer(), ec_gb_tree_node(K, V)}.
-type ec_gb_tree_node(K, V) :: 'nil' | {K, V,
ec_gb_tree_node(K, V),
ec_gb_tree_node(K, V)}.
%%%===================================================================
%%% API
%%%===================================================================
@ -36,7 +48,7 @@
%% same implementation is created and returned.
%%
%% @param ModuleName|Object The module name or existing dictionary object.
-spec new() -> gb_trees:tree(_K, _V).
-spec new() -> dictionary(_K, _V).
new() ->
gb_trees:empty().
@ -44,7 +56,7 @@ new() ->
%%
%% @param Object The dictory object to check
%% @param Key The key to check the dictionary for
-spec has_key(ec_dictionary:key(K), Object::gb_trees:tree(K, _V)) -> boolean().
-spec has_key(ec_dictionary:key(K), Object::dictionary(K, _V)) -> boolean().
has_key(Key, Data) ->
case gb_trees:lookup(Key, Data) of
{value, _Val} ->
@ -59,7 +71,7 @@ has_key(Key, Data) ->
%% @param Object The dictionary object to return the value from
%% @param Key The key requested
%% when the key does not exist @throws not_found
-spec get(ec_dictionary:key(K), Object::gb_trees:tree(K, V)) ->
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
ec_dictionary:value(V).
get(Key, Data) ->
case gb_trees:lookup(Key, Data) of
@ -71,7 +83,7 @@ get(Key, Data) ->
-spec get(ec_dictionary:key(K),
ec_dictionary:value(V),
Object::gb_trees:tree(K, V)) ->
Object::dictionary(K, V)) ->
ec_dictionary:value(V).
get(Key, Default, Data) ->
case gb_trees:lookup(Key, Data) of
@ -88,8 +100,8 @@ get(Key, Default, Data) ->
%% @param Key the key to add
%% @param Value the value to add
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
Object::gb_trees:tree(K, V)) ->
gb_trees:tree(K, V).
Object::dictionary(K, V)) ->
dictionary(K, V).
add(Key, Value, Data) ->
gb_trees:enter(Key, Value, Data).
@ -98,8 +110,8 @@ add(Key, Value, Data) ->
%%
%% @param Object the dictionary object to remove the value from
%% @param Key the key of the key/value pair to remove
-spec remove(ec_dictionary:key(K), Object::gb_trees:tree(K, V)) ->
gb_trees:tree(K, V).
-spec remove(ec_dictionary:key(K), Object::dictionary(K, V)) ->
dictionary(K, V).
remove(Key, Data) ->
gb_trees:delete_any(Key, Data).
@ -107,24 +119,24 @@ remove(Key, Data) ->
%%
%% @param Object the dictionary object to check
%% @param Value The value to check if exists
-spec has_value(ec_dictionary:value(V), Object::gb_trees:tree(_K, V)) -> boolean().
-spec has_value(ec_dictionary:value(V), Object::dictionary(_K, V)) -> boolean().
has_value(Value, Data) ->
lists:member(Value, gb_trees:values(Data)).
%% @doc return the current number of key value pairs in the dictionary
%%
%% @param Object the object return the size for.
-spec size(Object::gb_trees:tree(_K, _V)) -> non_neg_integer().
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
size(Data) ->
gb_trees:size(Data).
-spec to_list(gb_trees:tree(K, V)) -> [{ec_dictionary:key(K),
-spec to_list(dictionary(K, V)) -> [{ec_dictionary:key(K),
ec_dictionary:value(V)}].
to_list(Data) ->
gb_trees:to_list(Data).
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
gb_trees:tree(K, V).
dictionary(K, V).
from_list(List) when is_list(List) ->
lists:foldl(fun({Key, Value}, Dict) ->
gb_trees:enter(Key, Value, Dict)
@ -132,6 +144,82 @@ from_list(List) when is_list(List) ->
gb_trees:empty(),
List).
-spec keys(gb_trees:tree(K,_V)) -> [ec_dictionary:key(K)].
-spec keys(dictionary(K,_V)) -> [ec_dictionary:key(K)].
keys(Data) ->
gb_trees:keys(Data).
%%%===================================================================
%%% Tests
%%%===================================================================
-ifndef(NOTEST).
-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.

View file

@ -1,107 +0,0 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.
%%% @doc
%%% This provides an implementation of the ec_vsn for git. That is
%%% it is capable of returning a semver for a git repository
%%% see ec_vsn
%%% see ec_semver
%%% @end
%%%-------------------------------------------------------------------
-module(ec_git_vsn).
-behaviour(ec_vsn).
%% API
-export([new/0,
vsn/1]).
-ifdef(TEST).
-export([parse_tags/1,
get_patch_count/1,
collect_default_refcount/1
]).
-endif.
-export_type([t/0]).
%%%===================================================================
%%% Types
%%%===================================================================
%% 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() :: {}.
%%%===================================================================
%%% API
%%%===================================================================
-spec new() -> t().
new() ->
{}.
-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)}.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
collect_default_refcount(Data) ->
%% Get the tag timestamp and minimal ref from the system. The
%% timestamp is really important from an ordering perspective.
RawRef = os:cmd("git log -n 1 --pretty=format:'%h\n' "),
{Tag, TagVsn} = parse_tags(Data),
RawCount =
case Tag of
undefined ->
os:cmd("git rev-list --count HEAD");
_ ->
get_patch_count(Tag)
end,
{TagVsn, RawRef, RawCount}.
build_vsn_string(Vsn, RawRef, RawCount) ->
%% Cleanup the tag and the Ref information. Basically leading 'v's and
%% whitespace needs to go away.
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
case Count of
<<"0">> ->
erlang:binary_to_list(erlang:iolist_to_binary(Vsn));
_ ->
erlang:binary_to_list(erlang:iolist_to_binary([Vsn, "+build.",
Count, RefTag]))
end.
get_patch_count(RawRef) ->
Ref = re:replace(RawRef, "\\s", "", [global]),
Cmd = io_lib:format("git rev-list --count ~ts..HEAD",
[Ref]),
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.

View file

@ -1,4 +1,3 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
@ -52,7 +51,7 @@ find(_Fun, []) ->
error.
%% @doc Fetch a value from the list. If the function returns true the
%% value is returned. If processing reaches the end of the list and
%% value is returend. 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,3 +62,184 @@ fetch(Fun, List) when is_list(List), is_function(Fun) ->
error ->
throw(not_found)
end.
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifndef(NOTEST).
-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.

View file

@ -1,4 +1,3 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2011 Erlware, LLC.

View file

@ -1,5 +1,4 @@
%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
%%% vi:ts=4 sw=4 et
%%% The MIT License
%%%
%%% Copyright (c) 2007 Stephen Marsh
@ -30,7 +29,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 parallelizing inside a cluster. It
%%% and across erlang nodes, for parallizing 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 +37,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 parallelize the operation.
%%% to parallize the operation.
%%%
%%% fold is the one exception, parallel fold is different from linear
%%% fold. This module also include a simple mapreduce implementation,
@ -169,7 +168,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 occurring in the calling process.
%%% results in the error occuring in the calling process.
%%%
-module(ec_plists).
@ -217,13 +216,13 @@
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec all(el_fun(), list()) -> boolean().
-spec all/2 :: (el_fun(), list()) -> boolean().
all(Fun, List) ->
all(Fun, List, 1).
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec all(el_fun(), list(), malt()) -> boolean().
-spec all/3 :: (el_fun(), list(), malt()) -> boolean().
all(Fun, List, Malt) ->
try
runmany(fun (L) ->
@ -247,13 +246,13 @@ all(Fun, List, Malt) ->
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec any(fun(), list()) -> boolean().
-spec any/2 :: (fun(), list()) -> boolean().
any(Fun, List) ->
any(Fun, List, 1).
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec any(fun(), list(), malt()) -> boolean().
-spec any/3 :: (fun(), list(), malt()) -> boolean().
any(Fun, List, Malt) ->
try
runmany(fun (L) ->
@ -276,13 +275,13 @@ any(Fun, List, Malt) ->
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec filter(fun(), list()) -> list().
-spec filter/2 :: (fun(), list()) -> list().
filter(Fun, List) ->
filter(Fun, List, 1).
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec filter(fun(), list(), malt()) -> list().
-spec filter/3 :: (fun(), list(), malt()) -> list().
filter(Fun, List, Malt) ->
runmany(fun (L) ->
lists:filter(Fun, L)
@ -297,12 +296,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(fun(), InitAcc::term(), list()) -> term().
-spec fold/3 :: (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(fun(), InitAcc::term(), list(), malt()) -> term().
-spec fold/4 :: (fun(), InitAcc::term(), list(), malt()) -> term().
fold(Fun, InitAcc, List, Malt) ->
fold(Fun, Fun, InitAcc, List, Malt).
@ -323,24 +322,24 @@ fold(Fun, InitAcc, List, Malt) ->
%%
%% Malt is the malt for the initial folding of sublists, and for the
%% possible recursive fuse.
-spec fold(fun(), fuse(), InitAcc::term(), list(), malt()) -> term().
-spec fold/5 :: (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 Similar to foreach in module
%% @doc Similiar to foreach in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>
%% except it makes no guarantee about the order it processes list elements.
-spec foreach(fun(), list()) -> ok.
-spec foreach/2 :: (fun(), list()) -> ok.
foreach(Fun, List) ->
foreach(Fun, List, 1).
%% @doc Similar to foreach in module
%% @doc Similiar to foreach in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>
%% except it makes no guarantee about the order it processes list elements.
-spec foreach(fun(), list(), malt()) -> ok.
-spec foreach/3 :: (fun(), list(), malt()) -> ok.
foreach(Fun, List, Malt) ->
runmany(fun (L) ->
lists:foreach(Fun, L)
@ -352,13 +351,13 @@ foreach(Fun, List, Malt) ->
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec map(fun(), list()) -> list().
-spec map/2 :: (fun(), list()) -> list().
map(Fun, List) ->
map(Fun, List, 1).
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec map(fun(), list(), malt()) -> list().
-spec map/3 :: (fun(), list(), malt()) -> list().
map(Fun, List, Malt) ->
runmany(fun (L) ->
lists:map(Fun, L)
@ -369,7 +368,7 @@ map(Fun, List, Malt) ->
List, Malt).
%% @doc values are returned as {value, term()}.
-spec ftmap(fun(), list()) -> list().
-spec ftmap/2 :: (fun(), list()) -> list().
ftmap(Fun, List) ->
map(fun(L) ->
try
@ -381,7 +380,7 @@ ftmap(Fun, List) ->
end, List).
%% @doc values are returned as {value, term()}.
-spec ftmap(fun(), list(), malt()) -> list().
-spec ftmap/3 :: (fun(), list(), malt()) -> list().
ftmap(Fun, List, Malt) ->
map(fun(L) ->
try
@ -394,13 +393,13 @@ ftmap(Fun, List, Malt) ->
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec partition(fun(), list()) -> {list(), list()}.
-spec partition/2 :: (fun(), list()) -> {list(), list()}.
partition(Fun, List) ->
partition(Fun, List, 1).
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec partition(fun(), list(), malt()) -> {list(), list()}.
-spec partition/3 :: (fun(), list(), malt()) -> {list(), list()}.
partition(Fun, List, Malt) ->
runmany(fun (L) ->
lists:partition(Fun, L)
@ -415,7 +414,7 @@ partition(Fun, List, Malt) ->
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec sort(list()) -> list().
-spec sort/1 :: (list()) -> list().
sort(List) ->
sort(fun (A, B) ->
A =< B
@ -424,7 +423,7 @@ sort(List) ->
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec sort(fun(), list()) -> list().
-spec sort/2 :: (fun(), list()) -> list().
sort(Fun, List) ->
sort(Fun, List, ?SORTMALT).
@ -432,10 +431,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 separate process, and each merging of results is done in a
%% separate process. Malt defaults to 100, causing the list to be split into
%% 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
%% 100-element sublists.
-spec sort(fun(), list(), malt()) -> list().
-spec sort/3 :: (fun(), list(), malt()) -> list().
sort(Fun, List, Malt) ->
Fun2 = fun (L) ->
lists:sort(Fun, L)
@ -447,7 +446,7 @@ sort(Fun, List, Malt) ->
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec usort(list()) -> list().
-spec usort/1 :: (list()) -> list().
usort(List) ->
usort(fun (A, B) ->
A =< B
@ -456,7 +455,7 @@ usort(List) ->
%% @doc Same semantics as in module
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
-spec usort(fun(), list()) -> list().
-spec usort/2 :: (fun(), list()) -> list().
usort(Fun, List) ->
usort(Fun, List, ?SORTMALT).
@ -464,12 +463,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 separate process, and each merging of results is done in a
%% separate process. Malt defaults to 100, causing the list to be split into
%% 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
%% 100-element sublists.
%%
%% usort removes duplicate elements while it sorts.
-spec usort(fun(), list(), malt()) -> list().
%% usort removes duplicate elments while it sorts.
-spec usort/3 :: (fun(), list(), malt()) -> list().
usort(Fun, List, Malt) ->
Fun2 = fun (L) ->
lists:usort(Fun, L)
@ -480,10 +479,9 @@ usort(Fun, List, Malt) ->
runmany(Fun2, {recursive, Fuse}, List, Malt).
%% @doc Like below, assumes default MapMalt of 1.
-spec mapreduce(MapFunc, list()) -> dict:dict() when
-spec mapreduce/2 :: (MapFunc, list()) -> dict() when
MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}.
mapreduce(MapFunc, List) ->
mapreduce(MapFunc, List, 1).
@ -507,11 +505,11 @@ 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 separate process.
%% meaning each element of the list is mapped by a seperate process.
%%
%% mapreduce requires OTP R11B, or it may leave monitoring messages in the
%% message queue.
-spec mapreduce(MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict:dict() when
-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()).
@ -572,8 +570,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 separate process.
-spec runmany(fun(), fuse(), list()) -> term().
%% meaning each element of the list is processed by a seperate process.
-spec runmany/3 :: (fun(), fuse(), list()) -> term().
runmany(Fun, Fuse, List) ->
runmany(Fun, Fuse, List, 1).
@ -601,14 +599,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 parallelized
%% process is spawned to fuse each pair of results. It is a parallized
%% 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(fun(([term()]) -> term()), fuse(), list(), malt()) -> term().
-spec runmany/4 :: (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);
@ -677,7 +675,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 separately
%% by default, operate on each element seperately
runmany(Fun, Fuse, List, Nodes, 1, []);
runmany(Fun, Fuse, List, local, Split, []) ->
List2 = splitmany(List, Split),
@ -758,8 +756,8 @@ receivefrom(Pid) ->
receive
{Pid, R} ->
R;
{'DOWN', _, _, Pid, Reason} when Reason =/= normal ->
erlang:throw({Pid, Reason});
{'DOWN', _, _, BadPid, Reason} when Reason =/= normal ->
erlang:throw({BadPid, Reason});
{timerrang, _} ->
erlang:throw({nil, timeout})
end.
@ -808,7 +806,20 @@ cluster_runmany(Fun, Fuse, [Task|TaskList], [N|Nodes], Running, Results) ->
Parent ! {erlang:self(), fuse, FuseFunc(R1, R2)}
end
end,
Fun3 = fun() -> runmany_wrap(Fun2, Parent) 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,
Pid = proc_lib:spawn(N, Fun3),
erlang:monitor(process, Pid),
cluster_runmany(Fun, Fuse, TaskList, Nodes, [{Pid, N, Task}|Running], Results);
@ -858,20 +869,6 @@ 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) ->

View file

@ -1,4 +1,3 @@
%%% vi:ts=4 sw=4 et
%%% Copyright (c) 2008 Robert Virding. All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
@ -32,7 +31,7 @@
%%% representation of a dictionary, where a red-black tree is used to
%%% store the keys and values.
%%%
%%% This module implements exactly the same interface as the module
%%% This module implents 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 +295,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 after (possibly) adding a node to the left/right.
%% Balance a tree afer (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)) ->

View file

@ -1,4 +1,3 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @copyright (C) 2011, Erlware LLC
%%% @doc
@ -202,13 +201,13 @@ pes(VsnA, VsnB) ->
%%%===================================================================
%%% Friend Functions
%%%===================================================================
%% @doc helper function for the peg grammar to parse the iolist into a semver
%% @doc helper function for the peg grammer 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 grammar to parse the iolist into a major_minor_patch
%% @doc helper function for the peg grammer 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 +223,7 @@ parse_major_minor_patch_minpatch([MajVsn,
[<<".">>, MinPatch]]) ->
{strip_maj_version(MajVsn), MinVsn, PatchVsn, MinPatch}.
%% @doc helper function for the peg grammar to parse the iolist into an alpha part
%% @doc helper function for the peg grammer to parse the iolist into an alpha part
-spec parse_alpha_part(iolist()) -> [alpha_part()].
parse_alpha_part([]) ->
[];
@ -287,25 +286,425 @@ 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}, Alpha})
internal_pes(VsnA, {{LM, LMI}, _})
when erlang:is_integer(LM),
erlang:is_integer(LMI) ->
gte(VsnA, {{LM, LMI, 0}, Alpha}) andalso
gte(VsnA, {{LM, LMI, 0}, {[], []}}) andalso
lt(VsnA, {{LM + 1, 0, 0, 0}, {[], []}});
internal_pes(VsnA, {{LM, LMI, LP}, Alpha})
internal_pes(VsnA, {{LM, LMI, LP}, _})
when erlang:is_integer(LM),
erlang:is_integer(LMI),
erlang:is_integer(LP) ->
gte(VsnA, {{LM, LMI, LP}, Alpha})
gte(VsnA, {{LM, LMI, LP}, {[], []}})
andalso
lt(VsnA, {{LM, LMI + 1, 0, 0}, {[], []}});
internal_pes(VsnA, {{LM, LMI, LP, LMP}, Alpha})
internal_pes(VsnA, {{LM, LMI, LP, LMP}, _})
when erlang:is_integer(LM),
erlang:is_integer(LMI),
erlang:is_integer(LP),
erlang:is_integer(LMP) ->
gte(VsnA, {{LM, LMI, LP, LMP}, Alpha})
gte(VsnA, {{LM, LMI, LP, LMP}, {[], []}})
andalso
lt(VsnA, {{LM, LMI, LP + 1, 0}, {[], []}});
internal_pes(Vsn, LVsn) ->
gte(Vsn, LVsn).
%%%===================================================================
%%% Test Functions
%%%===================================================================
-ifndef(NOTEST).
-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.

View file

@ -1,302 +0,0 @@
-module(ec_semver_parser).
-export([parse/1,file/1]).
-define(p_anything,true).
-define(p_charclass,true).
-define(p_choose,true).
-define(p_not,true).
-define(p_one_or_more,true).
-define(p_optional,true).
-define(p_scan,true).
-define(p_seq,true).
-define(p_string,true).
-define(p_zero_or_more,true).
-spec file(file:name()) -> any().
file(Filename) -> case file:read_file(Filename) of {ok,Bin} -> parse(Bin); Err -> Err end.
-spec parse(binary() | list()) -> any().
parse(List) when is_list(List) -> parse(unicode:characters_to_binary(List));
parse(Input) when is_binary(Input) ->
_ = setup_memo(),
Result = case 'semver'(Input,{{line,1},{column,1}}) of
{AST, <<>>, _Index} -> AST;
Any -> Any
end,
release_memo(), Result.
-spec 'semver'(input(), index()) -> parse_result().
'semver'(Input, Index) ->
p(Input, Index, 'semver', fun(I,D) -> (p_seq([fun 'major_minor_patch_min_patch'/2, p_optional(p_seq([p_string(<<"-">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_optional(p_seq([p_string(<<"+">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_not(p_anything())]))(I,D) end, fun(Node, _Idx) -> ec_semver:internal_parse_version(Node) end).
-spec 'major_minor_patch_min_patch'(input(), index()) -> parse_result().
'major_minor_patch_min_patch'(Input, Index) ->
p(Input, Index, 'major_minor_patch_min_patch', fun(I,D) -> (p_seq([p_choose([p_seq([p_optional(p_string(<<"v">>)), fun 'numeric_part'/2]), fun 'alpha_part'/2]), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2]))]))(I,D) end, fun(Node, Idx) ->transform('major_minor_patch_min_patch', Node, Idx) end).
-spec 'version_part'(input(), index()) -> parse_result().
'version_part'(Input, Index) ->
p(Input, Index, 'version_part', fun(I,D) -> (p_choose([fun 'numeric_part'/2, fun 'alpha_part'/2]))(I,D) end, fun(Node, Idx) ->transform('version_part', Node, Idx) end).
-spec 'numeric_part'(input(), index()) -> parse_result().
'numeric_part'(Input, Index) ->
p(Input, Index, 'numeric_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[0-9]">>)))(I,D) end, fun(Node, _Idx) ->erlang:list_to_integer(erlang:binary_to_list(erlang:iolist_to_binary(Node))) end).
-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).
transform(_,Node,_Index) -> Node.
-type index() :: {{line, pos_integer()}, {column, pos_integer()}}.
-type input() :: binary().
-type parse_failure() :: {fail, term()}.
-type parse_success() :: {term(), input(), index()}.
-type parse_result() :: parse_failure() | parse_success().
-type parse_fun() :: fun((input(), index()) -> parse_result()).
-type xform_fun() :: fun((input(), index()) -> term()).
-spec p(input(), index(), atom(), parse_fun(), xform_fun()) -> parse_result().
p(Inp, StartIndex, Name, ParseFun, TransformFun) ->
case get_memo(StartIndex, Name) of % See if the current reduction is memoized
{ok, Memo} -> %Memo; % If it is, return the stored result
Memo;
_ -> % If not, attempt to parse
Result = case ParseFun(Inp, StartIndex) of
{fail,_} = Failure -> % If it fails, memoize the failure
Failure;
{Match, InpRem, NewIndex} -> % If it passes, transform and memoize the result.
Transformed = TransformFun(Match, StartIndex),
{Transformed, InpRem, NewIndex}
end,
memoize(StartIndex, Name, Result),
Result
end.
-spec setup_memo() -> ets:tid().
setup_memo() ->
put({parse_memo_table, ?MODULE}, ets:new(?MODULE, [set])).
-spec release_memo() -> true.
release_memo() ->
ets:delete(memo_table_name()).
-spec memoize(index(), atom(), parse_result()) -> true.
memoize(Index, Name, Result) ->
Memo = case ets:lookup(memo_table_name(), Index) of
[] -> [];
[{Index, Plist}] -> Plist
end,
ets:insert(memo_table_name(), {Index, [{Name, Result}|Memo]}).
-spec get_memo(index(), atom()) -> {ok, term()} | {error, not_found}.
get_memo(Index, Name) ->
case ets:lookup(memo_table_name(), Index) of
[] -> {error, not_found};
[{Index, Plist}] ->
case proplists:lookup(Name, Plist) of
{Name, Result} -> {ok, Result};
_ -> {error, not_found}
end
end.
-spec memo_table_name() -> ets:tid().
memo_table_name() ->
get({parse_memo_table, ?MODULE}).
-ifdef(p_eof).
-spec p_eof() -> parse_fun().
p_eof() ->
fun(<<>>, Index) -> {eof, [], Index};
(_, Index) -> {fail, {expected, eof, Index}} end.
-endif.
-ifdef(p_optional).
-spec p_optional(parse_fun()) -> parse_fun().
p_optional(P) ->
fun(Input, Index) ->
case P(Input, Index) of
{fail,_} -> {[], Input, Index};
{_, _, _} = Success -> Success
end
end.
-endif.
-ifdef(p_not).
-spec p_not(parse_fun()) -> parse_fun().
p_not(P) ->
fun(Input, Index)->
case P(Input,Index) of
{fail,_} ->
{[], Input, Index};
{Result, _, _} -> {fail, {expected, {no_match, Result},Index}}
end
end.
-endif.
-ifdef(p_assert).
-spec p_assert(parse_fun()) -> parse_fun().
p_assert(P) ->
fun(Input,Index) ->
case P(Input,Index) of
{fail,_} = Failure-> Failure;
_ -> {[], Input, Index}
end
end.
-endif.
-ifdef(p_seq).
-spec p_seq([parse_fun()]) -> parse_fun().
p_seq(P) ->
fun(Input, Index) ->
p_all(P, Input, Index, [])
end.
-spec p_all([parse_fun()], input(), index(), [term()]) -> parse_result().
p_all([], Inp, Index, Accum ) -> {lists:reverse( Accum ), Inp, Index};
p_all([P|Parsers], Inp, Index, Accum) ->
case P(Inp, Index) of
{fail, _} = Failure -> Failure;
{Result, InpRem, NewIndex} -> p_all(Parsers, InpRem, NewIndex, [Result|Accum])
end.
-endif.
-ifdef(p_choose).
-spec p_choose([parse_fun()]) -> parse_fun().
p_choose(Parsers) ->
fun(Input, Index) ->
p_attempt(Parsers, Input, Index, none)
end.
-spec p_attempt([parse_fun()], input(), index(), none | parse_failure()) -> parse_result().
p_attempt([], _Input, _Index, Failure) -> Failure;
p_attempt([P|Parsers], Input, Index, FirstFailure)->
case P(Input, Index) of
{fail, _} = Failure ->
case FirstFailure of
none -> p_attempt(Parsers, Input, Index, Failure);
_ -> p_attempt(Parsers, Input, Index, FirstFailure)
end;
Result -> Result
end.
-endif.
-ifdef(p_zero_or_more).
-spec p_zero_or_more(parse_fun()) -> parse_fun().
p_zero_or_more(P) ->
fun(Input, Index) ->
p_scan(P, Input, Index, [])
end.
-endif.
-ifdef(p_one_or_more).
-spec p_one_or_more(parse_fun()) -> parse_fun().
p_one_or_more(P) ->
fun(Input, Index)->
Result = p_scan(P, Input, Index, []),
case Result of
{[_|_], _, _} ->
Result;
_ ->
{fail, {expected, Failure, _}} = P(Input,Index),
{fail, {expected, {at_least_one, Failure}, Index}}
end
end.
-endif.
-ifdef(p_label).
-spec p_label(atom(), parse_fun()) -> parse_fun().
p_label(Tag, P) ->
fun(Input, Index) ->
case P(Input, Index) of
{fail,_} = Failure ->
Failure;
{Result, InpRem, NewIndex} ->
{{Tag, Result}, InpRem, NewIndex}
end
end.
-endif.
-ifdef(p_scan).
-spec p_scan(parse_fun(), input(), index(), [term()]) -> {[term()], input(), index()}.
p_scan(_, <<>>, Index, Accum) -> {lists:reverse(Accum), <<>>, Index};
p_scan(P, Inp, Index, Accum) ->
case P(Inp, Index) of
{fail,_} -> {lists:reverse(Accum), Inp, Index};
{Result, InpRem, NewIndex} -> p_scan(P, InpRem, NewIndex, [Result | Accum])
end.
-endif.
-ifdef(p_string).
-spec p_string(binary()) -> parse_fun().
p_string(S) ->
Length = erlang:byte_size(S),
fun(Input, Index) ->
try
<<S:Length/binary, Rest/binary>> = Input,
{S, Rest, p_advance_index(S, Index)}
catch
error:{badmatch,_} -> {fail, {expected, {string, S}, Index}}
end
end.
-endif.
-ifdef(p_anything).
-spec p_anything() -> parse_fun().
p_anything() ->
fun(<<>>, Index) -> {fail, {expected, any_character, Index}};
(Input, Index) when is_binary(Input) ->
<<C/utf8, Rest/binary>> = Input,
{<<C/utf8>>, Rest, p_advance_index(<<C/utf8>>, Index)}
end.
-endif.
-ifdef(p_charclass).
-spec p_charclass(string() | binary()) -> parse_fun().
p_charclass(Class) ->
{ok, RE} = re:compile(Class, [unicode, dotall]),
fun(Inp, Index) ->
case re:run(Inp, RE, [anchored]) of
{match, [{0, Length}|_]} ->
{Head, Tail} = erlang:split_binary(Inp, Length),
{Head, Tail, p_advance_index(Head, Index)};
_ -> {fail, {expected, {character_class, binary_to_list(Class)}, Index}}
end
end.
-endif.
-ifdef(p_regexp).
-spec p_regexp(binary()) -> parse_fun().
p_regexp(Regexp) ->
{ok, RE} = re:compile(Regexp, [unicode, dotall, anchored]),
fun(Inp, Index) ->
case re:run(Inp, RE) of
{match, [{0, Length}|_]} ->
{Head, Tail} = erlang:split_binary(Inp, Length),
{Head, Tail, p_advance_index(Head, Index)};
_ -> {fail, {expected, {regexp, binary_to_list(Regexp)}, Index}}
end
end.
-endif.
-ifdef(line).
-spec line(index() | term()) -> pos_integer() | undefined.
line({{line,L},_}) -> L;
line(_) -> undefined.
-endif.
-ifdef(column).
-spec column(index() | term()) -> pos_integer() | undefined.
column({_,{column,C}}) -> C;
column(_) -> undefined.
-endif.
-spec p_advance_index(input() | unicode:charlist() | pos_integer(), index()) -> index().
p_advance_index(MatchedInput, Index) when is_list(MatchedInput) orelse is_binary(MatchedInput)-> % strings
lists:foldl(fun p_advance_index/2, Index, unicode:characters_to_list(MatchedInput));
p_advance_index(MatchedInput, Index) when is_integer(MatchedInput) -> % single characters
{{line, Line}, {column, Col}} = Index,
case MatchedInput of
$\n -> {{line, Line+1}, {column, 1}};
_ -> {{line, Line}, {column, Col+1}}
end.

View file

@ -7,3 +7,8 @@ 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).`

View file

@ -1,5 +1,4 @@
%% -*- mode: Erlang; fill-column: 79; comment-column: 70; -*-
%% vi:ts=4 sw=4 et
%%%---------------------------------------------------------------------------
%%% Permission is hereby granted, free of charge, to any person
%%% obtaining a copy of this software and associated documentation
@ -39,15 +38,12 @@
say/1,
say/2]).
-ifdef(TEST).
-export([get_boolean/1,
get_integer/1]).
-endif.
-export_type([prompt/0,
type/0,
supported/0]).
-include_lib("eunit/include/eunit.hrl").
%%============================================================================
%% Types
%%============================================================================
@ -80,7 +76,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. Tries to return the
%% @doc Asks the user to respond to the prompt. Trys to return the
%% value in the format specified by 'Type'.
-spec ask(prompt(), type()) -> supported().
ask(Prompt, boolean) ->
@ -88,9 +84,9 @@ ask(Prompt, boolean) ->
ask(Prompt, number) ->
ask_convert(Prompt, fun get_integer/1, number, none);
ask(Prompt, string) ->
ask_convert(Prompt, fun get_string/1, string, none).
ask_convert(Prompt, fun get_integer/1, string, none).
%% @doc Asks the user to respond to the prompt. Tries to return the
%% @doc Asks the user to respond to the prompt. Trys to return the
%% value in the format specified by 'Type'.
-spec ask_default(prompt(), type(), supported()) -> supported().
ask_default(Prompt, boolean, Default) ->
@ -132,7 +128,7 @@ ask_convert(Prompt, TransFun, Type, Default) ->
Default ->
[" (", io_lib:format("~p", [Default]) , ")"]
end, "> "])),
Data = string:trim(string:trim(io:get_line(NewPrompt)), both, [$\n]),
Data = string:strip(string:strip(io:get_line(NewPrompt)), both, $\n),
Ret = TransFun(Data),
case Ret of
no_data ->
@ -150,7 +146,7 @@ ask_convert(Prompt, TransFun, Type, Default) ->
Ret
end.
%% @doc Tries to translate the result into a boolean
%% @doc Trys to translate the result into a boolean
-spec get_boolean(string()) -> boolean().
get_boolean([]) ->
no_data;
@ -177,7 +173,7 @@ get_boolean([$N | _]) ->
get_boolean(_) ->
no_clue.
%% @doc Tries to translate the result into an integer
%% @doc Trys to translate the result into an integer
-spec get_integer(string()) -> integer().
get_integer([]) ->
no_data;
@ -201,3 +197,21 @@ get_string(String) ->
false ->
no_clue
end.
%%%====================================================================
%%% tests
%%%====================================================================
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"))].

View file

@ -1,51 +0,0 @@
%%% vi:ts=4 sw=4 et
%%%-------------------------------------------------------------------
%%% @author Eric Merritt <ericbmerritt@gmail.com>
%%% @copyright 2014 Erlware, LLC.
%%% @doc
%%% Provides a signature to manage returning semver formatted versions
%%% from various version control repositories.
%%%
%%% This interface is a member of the Erlware Commons Library.
%%% @end
%%%-------------------------------------------------------------------
-module(ec_vsn).
%% API
-export([new/1,
vsn/1]).
-export_type([t/0]).
%%%===================================================================
%%% Types
%%%===================================================================
-record(t, {callback, data}).
%% 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() :: #t{}.
-callback new() -> any().
-callback vsn(any()) -> {ok, string()} | {error, Reason::any()}.
%%%===================================================================
%%% API
%%%===================================================================
%% @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()) -> t().
new(ModuleName) when erlang:is_atom(ModuleName) ->
#t{callback = ModuleName, data = ModuleName:new()}.
%% @doc Return the semver or an error depending on what is possible
%% with this implementation in this directory.
%%
%% @param The dictionary object
-spec vsn(t()) -> {ok, string()} | {error, Reason::any()}.
vsn(#t{callback = Mod, data = Data}) ->
Mod:vsn(Data).

View file

@ -1,11 +1,7 @@
%% -*- mode: Erlang; fill-column: 75; comment-column: 50; -*-
{application, erlware_commons,
[{description, "Additional standard library for Erlang"},
{vsn,"git"},
{vsn, "semver"},
{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"}]}]}.
{applications, [kernel, stdlib]}]}.

View file

@ -1,39 +0,0 @@
%%% @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")).

View file

@ -1,28 +0,0 @@
%%% @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)).

View file

@ -0,0 +1,223 @@
%% compile with
%% erl -pz ebin --make
%% start test with
%% erl -pz ebin -pz test
%% proper:module(ec_dictionary_proper).
-module(ec_dictionary_proper).
-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)).

View file

@ -1,84 +0,0 @@
%%% @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]).

View file

@ -1,67 +0,0 @@
%%% @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)).

View file

@ -1,13 +0,0 @@
%%% @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")).

View file

@ -1,172 +0,0 @@
%%% @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).

View file

@ -73,12 +73,3 @@ ftmap_bad_test() ->
lists:seq(1, 5)),
?assertMatch([{value, 1}, {error,{throw,test_exception}}, {value, 3},
{value, 4}, {value, 5}] , Results).
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).

View file

@ -1,447 +0,0 @@
%%% @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, {[],[]}}))).

View file

@ -1,19 +0,0 @@
%%% @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"))].

10
test/mock.erl Normal file
View file

@ -0,0 +1,10 @@
-module(mock).
-export([new_dictionary/0]).
new_dictionary() ->
meck:new(ec_dictionary_proper),
meck:expect(ec_dictionary_proper, dictionary, fun() ->
proper_types:union([ec_dict])
end).