text:
"""
    TestPaths

This module is intended to be used for testing new path types to
ensure that they are adhering to the AbstractPath API.

# Example

```julia
# Create a PathSet
ps = PathSet(; symlink=true)

# Select the subset of tests to run
# Inspect TestPaths.TESTALL to see full list
testsets = [
    test_registration,
    test_show,
    test_cmd,
    test_parse,
    test_convert,
    test_components,
    test_parents,
    test_join,
    test_splitext,
    test_basename,
    test_filename,
    test_extensions,
    test_isempty,
    test_normalize,
    test_canonicalize,
    test_relative,
    test_absolute,
    test_isdir,
    test_isfile,
    test_stat,
    test_filesize,
    test_modified,
    test_created,
    test_cd,
    test_readpath,
    test_walkpath,
    test_read,
    test_write,
    test_mkdir,
    test_cp,
    test_mv,
    test_sync,
    test_symlink,
    test_touch,
    test_tmpname,
    test_tmpdir,
    test_mktmp,
    test_mktmpdir,
    test_download,
]

# Run all the tests
test(ps, testsets)
```
"""
module TestPaths
    using Dates: Dates
    using FilePathsBase
    using FilePathsBase: /, join
    using Test

    export PathSet,
        TESTALL,
        test,
        test_registration,
        test_show,
        test_cmd,
        test_parse,
        test_convert,
        test_components,
        test_indexing,
        test_iteration,
        test_parents,
        test_descendants_and_ascendants,
        test_join,
        test_splitext,
        test_basename,
        test_filename,
        test_extensions,
        test_isempty,
        test_normalize,
        test_canonicalize,
        test_relative,
        test_absolute,
        test_isdir,
        test_isfile,
        test_stat,
        test_filesize,
        test_modified,
        test_created,
        test_issocket,
        test_isfifo,
        test_ischardev,
        test_isblockdev,
        test_ismount,
        test_isexecutable,
        test_isreadable,
        test_iswritable,
        test_cd,
        test_readpath,
        test_walkpath,
        test_read,
        test_write,
        test_mkdir,
        test_cp,
        test_mv,
        test_sync,
        test_symlink,
        test_touch,
        test_tmpname,
        test_tmpdir,
        test_mktmp,
        test_mktmpdir,
        test_chown,
        test_chmod,
        test_download

    """
        PathSet(root::AbstractPath=tmpdir(); symlink=false)

    Constructs a common test path hierarchy to running shared API tests.

    Hierarchy:

    ```
    root
    |-- foo
    |   |-- baz.txt
    |-- bar
    |   |-- qux
    |       |-- quux.tar.gz
    |-- fred
    |   |-- plugh
    ```
    """
    struct PathSet{P<:AbstractPath}
        root::P
        foo::P
        baz::P
        bar::P
        qux::P
        quux::P
        fred::P
        plugh::P
        link::Bool
    end

    function PathSet(root=tmpdir() / "pathset_root"; symlink=false)
        root = absolute(root)

        PathSet(
            root,
            root / "foo",
            root / "foo" / "baz.txt",
            root / "bar",
            root / "bar" / "qux",
            root / "bar" / "qux" / "quux.tar.gz",
            root / "fred",
            root / "fred" / "plugh",
            symlink,
        )
    end

    function initialize(ps::PathSet)
        @info "Initializing $(typeof(ps))"
        mkdir.([ps.foo, ps.qux, ps.fred]; recursive=true, exist_ok=true)
        write(ps.baz, "Hello World!")
        write(ps.quux, "Hello Again!")

        # If link is true then plugh is a symlink to foo
        if ps.link
            symlink(ps.foo, ps.plugh)
        else
            touch(ps.plugh)
        end
    end

    # NOTE: Most paths should test their own constructors as necessary.

    function test_registration(ps::PathSet{P}) where P <: AbstractPath
        @testset "Path constructor" begin
            str = string(ps.root)
            @test tryparse(P, str) !== nothing
            @test Path(str) == ps.root
            @test p"foo/bar" == Path("foo/bar")
        end
    end

    function test_show(ps::PathSet)
        @testset "show" begin
            str = string(ps.root)
            # For windows paths
            str = replace(str, "\\" => "/")
            @test sprint(show, ps.root; context=:compat => true) == "p\"$str\""
            # TODO: Figure out why this is broken.
            @test_broken sprint(show, ps.root; context=:compat => false) == str
        end
    end

    function test_cmd(ps::PathSet)
        @testset "cmd" begin
            str = string(ps.root)
            @test `echo $str` == `echo $(ps.root)`
        end
    end

    function test_parse(ps::PathSet{P}) where P <: AbstractPath
        @testset "parsing" begin
            str = string(ps.root)
            @test parse(P, str) == ps.root
            @test tryparse(P, str) == ps.root
        end
    end

    function test_convert(ps::PathSet{P}) where P <: AbstractPath
        @testset "convert" begin
            str = string(ps.root)
            @test convert(P, str) == ps.root
            @test convert(String, ps.root) == str
        end
    end

    function test_components(ps::PathSet)
        @testset "components" begin
            str = string(ps.root)
            @test ps.root.anchor == ps.root.drive * ps.root.root
            @test ps.quux.segments[end-2:end] == ("bar", "qux", "quux.tar.gz")

            # Check that isless on the path segments works
            @test ps.bar < ps.foo
            @test sort([ps.foo, ps.bar, ps.fred]) == [ps.bar, ps.foo, ps.fred]
        end
    end

    function test_indexing(ps::PathSet)
        @test firstindex(ps.root) == 1
        @test lastindex(ps.root) == length(ps.root.segments)
        # `begin` indexing was only added in 1.4, so we'll leave this test commented
        # out for now as there isn't a compat for that syntax.
        # @test ps.root[begin] == ps.root.segments[1]
        @test ps.baz[end] == "baz.txt"
        @test ps.quux[end-2:end] == ("bar", "qux", "quux.tar.gz")
        @test ps.quux[:] == ps.quux.segments[:]
    end

    function test_iteration(ps::PathSet)
        @test eltype(ps.root) == String
        @test tuple(ps.quux...) == ps.quux.segments
        @test all(in(ps.quux), ("bar", "qux", "quux.tar.gz"))
    end

    function test_parents(ps::PathSet)
        @testset "parents" begin
            @test parent(ps.foo) == ps.root
            @test parent(ps.qux) == ps.bar
            @test dirname(ps.foo) == ps.root

            @test hasparent(ps.qux)
            _parents = parents(ps.qux)
            @test _parents[end] == ps.bar
            @test _parents[end - 1] == ps.root
            @test _parents[1] == Path(ps.root; segments=())

            # More abstract path tests for edge cases when no parent exists
            @test hasparent(p"/foo")
            @test hasparent(p"./foo")
            @test hasparent(p"~/foo")
            @test !hasparent(p"foo")
            @test !hasparent(p"~")
            @test !hasparent(p".")
            @test !hasparent(p"/")

            # Test that relative paths with no parents return p"."
            @test parent(Path(basename(ps.foo))) == p"."

            # Test that parent on p"." should be ===
            path = p"."
            @test parent(path) === path

            # Test that parent on p"/" should be ===
            path = p"/"
            @test parent(path) === path

            # Test inclusion of root in parents
            relparents = parents(PosixPath(ps.root.segments, ""))
            absparents = parents(PosixPath(ps.root.segments, "/"))
            @test relparents !== absparents
            @test length(absparents) == length(relparents) + 1
            @test !in(absparents[1], relparents)
            @test absparents[1] == PosixPath((), "/")
        end
    end

    function test_descendants_and_ascendants(ps::PathSet)
        @testset "descendants and ascendants" begin
            # Test base cases
            @test isdescendant(p"/a/b", p"/")
            @test isdescendant(p"/a/b", p"/a")
            @test isdescendant(p"/a/b/c", p"/a")
            @test isdescendant(p"/a/b", p"/a/b")
            @test !isdescendant(p"/a/b", p"/a/b/c")
            @test !isdescendant(p"/a/b", p"/c")
            @test isascendant(p"/a", p"/a/b")
            @test !isascendant(p"/a/b", p"/a")

            # Test without our path types
            @test isdescendant(ps.foo, ps.root)
            @test isdescendant(ps.qux, ps.root)
            @test isdescendant(ps.foo, ps.foo)
            @test !isdescendant(ps.root, ps.foo)
            @test !isdescendant(ps.root, ps.qux)
            @test isascendant(ps.root, ps.foo)
            @test isascendant(ps.root, ps.quux)
            @test !isascendant(ps.qux, ps.root)
        end
    end

    function test_join(ps::PathSet)
        @testset "join" begin
            @test join(ps.root, "bar") == ps.bar
            @test ps.root / "foo" / "baz.txt" == ps.baz
            @test ps.root / "foobaz.txt" == ps.root / "foo" * "baz.txt"
            @test ps.root ./ ["foo", "bar"] == [ps.foo, ps.bar]
            @test ps.root / "foo/baz.txt" == ps.baz
            @test ps.root / p"foo/baz.txt" == ps.baz
            @test ps.root / p"foo" / p"baz.txt" == ps.baz
            @test ps.root / p"foo" / "baz.txt" == ps.baz

            # TODO: Maybe normalize this case for the user? {ps.root}/foo/./baz.txt
            @test normalize(ps.root / p"foo" / "" / "baz.txt") == ps.baz

            # Check that joining absolute paths matches base
            @test ps.root / p"/foo/baz.txt" == p"/foo/baz.txt"
        end
    end

    function test_splitext(ps::PathSet)
        @testset "splitext" begin
            @test splitext(ps.foo) == (ps.foo, "")
            @test splitext(ps.baz) == (ps.foo / "baz", ".txt")
            @test splitext(ps.quux) == (ps.qux / "quux.tar", ".gz")
        end
    end
    function test_basename(ps::PathSet)
        @testset "basename" begin
            @test basename(ps.foo) == "foo"
            @test basename(ps.baz) == "baz.txt"
            @test basename(ps.quux) == "quux.tar.gz"
        end
    end

    function test_filename(ps::PathSet)
        @testset "filename" begin
            @test filename(ps.foo) == "foo"
            @test filename(ps.baz) == "baz"
            @test filename(ps.quux) == "quux.tar"
        end
    end

    function test_extensions(ps::PathSet)
        @testset "extensions" begin
            @test extension(ps.foo) == ""
            @test extension(ps.baz) == "txt"
            @test extension(ps.quux) == "gz"
            @test extensions(ps.foo) == []
            @test extensions(ps.baz) == ["txt"]
            @test extensions(ps.quux) == ["tar", "gz"]
        end
    end

    function test_isempty(ps::PathSet{P}) where P <: AbstractPath
        @testset "isempty" begin
            @test !isempty(ps.foo)
            @test isempty(P())
        end
    end

    function test_normalize(ps::PathSet)
        @testset "normalize" begin
            @test normalize(ps.bar / ".." / "foo") == ps.foo
            @test normalize(ps.bar / ".") == ps.bar
            @test normpath(ps.bar / ".") == ps.bar

        end
    end

    function test_canonicalize(ps::PathSet)
        @testset "canonicalize" begin
            # NOTE: We call `canonicalize` on ps.bar in the `normalize` case because on
            # macOS the temp directory may include a symlink.
            @test canonicalize(ps.bar / ".." / "foo") == normalize(canonicalize(ps.bar) / ".." / "foo")
            @test canonicalize(ps.bar / ".") == normalize(canonicalize(ps.bar) / ".")
            @test realpath(ps.bar / ".") == canonicalize(ps.bar / ".")

            if ps.plugh !== nothing
                if isa(ps.plugh, WindowsPath) && VERSION < v"1.2"
                    @test_broken canonicalize(ps.plugh) == canonicalize(ps.foo)
                else
                    @test canonicalize(ps.plugh) == canonicalize(ps.foo)
                end
            end
        end
    end

    function test_relative(ps::PathSet)
        @testset "relative" begin
            @test relative(ps.foo, ps.qux).segments == ("..", "..", "foo")
        end
    end

    function test_absolute(ps::PathSet)
        @testset "absolute" begin
            @test isabsolute(ps.root) || isabsolute(absolute(ps.root))
            @test absolute(ps.root) == abspath(ps.root)
        end
    end

    function test_isdir(ps::PathSet)
        @testset "isdir" begin
            @test isdir(ps.foo)
            @test !isdir(ps.baz)
        end
    end

    function test_isfile(ps::PathSet)
        @testset "isfile" begin
            @test isfile(ps.baz)
            @test !isfile(ps.foo)
        end
    end

    function test_stat(ps::PathSet)
        @testset "stat" begin
            s = stat(ps.root)
            fields = fieldnames(typeof(s))

            @test :size in fields
            @test :ctime in fields
            @test :mtime in fields
            @test :user in fields
            @test :mode in fields

            if ps.link
                @test lstat(ps.plugh) != stat(ps.plugh)
            end

            str = sprint(show, s)
        end
    end

    # Minimal testing of issocket, isfifo, ischardev, isblockdev and ismount which
    # people won't typically include.
    function test_issocket(ps::PathSet)
        @test !issocket(ps.root)
    end

    function test_isfifo(ps::PathSet)
        @test !isfifo(ps.root)
    end

    function test_ischardev(ps::PathSet)
        @test !ischardev(ps.root)
    end

    function test_isblockdev(ps::PathSet)
        @test !isblockdev(ps.root)
    end

    function test_ismount(ps::PathSet)
        @test !ismount(ps.root)
    end

    function test_filesize(ps::PathSet)
        @testset "filesize" begin
            @test filesize(ps.baz) > 0
        end
    end

    function test_modified(ps::PathSet)
        @testset "modified" begin
            @test isa(modified(ps.baz), Dates.AbstractDateTime)
            @test modified(ps.baz) >= modified(ps.root)
        end
    end

    function test_created(ps::PathSet)
        @testset "created" begin
            @test isa(created(ps.baz), Dates.AbstractDateTime)
            @test created(ps.baz) >= created(ps.root)
        end
    end

    function test_isexecutable(ps::PathSet{P}) where P <: AbstractPath
        @testset "isexecutable" begin
            # I'm not entirely sure how to test this generally
            @test !isexecutable(ps.baz)

            # Directories should be executable for system paths
            if P <: SystemPath
                @test isexecutable(ps.foo)
            end
        end
    end

    function test_isreadable(ps::PathSet)
        @testset "isreadable" begin
            # Our test files should be readable by default
            @test isreadable(ps.baz)
            @test isreadable(ps.quux)

            # Our test directories should also be readable by default
            @test isreadable(ps.foo)
            @test isreadable(ps.qux)
        end
    end

    function test_iswritable(ps::PathSet)
        @testset "iswritable" begin
            # Our test files should be writable by default
            @test iswritable(ps.baz)
            @test iswritable(ps.quux)

            # Our test directories should also be writable by default
            @test iswritable(ps.foo)
            @test iswritable(ps.qux)
        end
    end

    function test_cd(ps::PathSet{P}) where P <: AbstractPath
        if P <: SystemPath
            @testset "cd" begin
                init_path = canonicalize(cwd())

                cd(ps.foo) do
                    cd_path = canonicalize(cwd())
                    @test cd_path != init_path
                    @test cd_path == canonicalize(ps.foo)
                end

                @test canonicalize(cwd()) == init_path

                cd(ps.qux)
                cd_path = canonicalize(cwd())
                @test cd_path != init_path
                @test cd_path == canonicalize(ps.qux)
                cd(init_path)
                @test canonicalize(cwd()) == init_path
            end
        end
    end

    function test_readpath(ps::PathSet)
        @testset "readpath" begin
            @test readdir(ps.root) == ["bar", "foo", "fred"]
            @test readdir(ps.qux) == ["quux.tar.gz"]
            @test readpath(ps.root) == [ps.bar, ps.foo, ps.fred]
            @test readpath(ps.qux) == [ps.quux]
        end
    end

    function test_walkpath(ps::PathSet{P}) where P
        @testset "walkpath" begin
            topdown = [ps.bar, ps.qux, ps.quux, ps.foo, ps.baz, ps.fred, ps.plugh]
            bottomup = [ps.quux, ps.qux, ps.bar, ps.baz, ps.foo, ps.plugh, ps.fred]

            @test collect(walkpath(ps.root; topdown=true)) == topdown
            @test collect(walkpath(ps.root; topdown=false)) == bottomup

            @test eltype(walkpath(ps.root)) == P  # should return a typed collection
        end
    end

    function test_read(ps::PathSet)
        @testset "read" begin
            @test read(ps.baz, String) == "Hello World!"
            open(ps.quux, "r") do io
                @test read(io, String) == "Hello Again!"
            end
        end
    end

    function test_write(ps::PathSet)
        @testset "write" begin
            write(ps.baz, "Goodbye World!")
            @test read(ps.baz, String) == "Goodbye World!"

            @testset "truncate" begin
                open(ps.quux, "w") do io
                    write(io, "Hello?")
                end
                @test read(ps.quux, String) == "Hello?"
            end

            @testset "append" begin
                open(ps.quux, "a") do io
                    write(io, " Did you need something?")
                end
                @test read(ps.quux, String) == "Hello? Did you need something?"
            end

            @testset "read/write" begin
                open(ps.quux, "w+") do io
                    write(io, "Goodnight!")
                    seekstart(io)
                    @test read(io, String) == "Goodnight!"
                end

                open(ps.quux, "a+") do io
                    write(io, " Zzzz")
                    seekstart(io)
                    @test read(io, String) == "Goodnight! Zzzz"
                end
            end
        end
    end

    function test_mkdir(ps::PathSet)
        @testset "mkdir" begin
            garply = ps.root / "corge" / "grault" / "garply"
            @test_throws ErrorException mkdir(garply)
            @test mkdir(garply; recursive=true) == garply
            @test exists(garply)
            @test_throws ErrorException mkdir(garply; recursive=true)
            @test mkdir(garply; recursive=true, exist_ok=true) == garply
            rm(ps.root / "corge"; recursive=true)
            @test !exists(garply)
        end
    end

    function test_cp(ps::PathSet)
        @testset "cp" begin
            cp(ps.foo, ps.qux / "foo"; force=true)
            @test exists(ps.qux / "foo" / "baz.txt")
            @test_throws ArgumentError cp(ps.foo, ps.qux / "foo")
            cp(ps.foo, ps.qux / "foo"; force=true)
            rm(ps.qux / "foo"; recursive=true)

            @testset "non-existent destination parent" begin
                # Test consistency with `cp` (e.g., destination parent must exist)
                dest = ps.root / "non-existent" / "destination"

                # The base behaviour can only be tested on system paths.
                if isa(dest, SystemPath)
                    if VERSION >= v"1.2"    # Error type changed in Julia 1.2
                        @test_throws ProcessFailedException run(`cp -r $(ps.foo) $dest`)
                    else
                        @test_throws ErrorException run(`cp -r $(ps.foo) $dest`)
                    end

                    if VERSION >= v"1.4"
                        @test_throws Base.IOError cp(string(ps.foo), string(dest))
                    else
                        @test_throws SystemError cp(string(ps.foo), string(dest))
                    end
                end

                # TODO: Use a more specific error type
                @test_throws ErrorException cp(ps.foo, dest)
            end
        end
    end

    function test_mv(ps::PathSet)
        @testset "mv" begin
            garply = ps.root / "corge" / "grault" / "garply"
            mkdir(garply; recursive=true, exist_ok=true)
            @test exists(garply)
            mv(ps.root / "corge", ps.foo / "corge"; force=true)
            @test exists(ps.foo / "corge" / "grault" / "garply")
            rm(ps.foo / "corge"; recursive=true)
        end
    end

    function test_sync(ps::PathSet)
        @testset "sync" begin
            @testset "empty destination" begin
                sync(ps.foo, ps.qux / "foo")
                @test exists(ps.qux / "foo" / "baz.txt")

                # Test that the copied baz file has a newer modified time
                baz_t = modified(ps.qux / "foo" / "baz.txt")
                @test modified(ps.baz) < baz_t
            end

            @testset "empty source" begin
                @test_throws ArgumentError sync(ps.root / "quux", ps.foo)
            end

            @testset "new source" begin
                # Don't cp unchanged files when a new file is added
                # NOTE: sleep before we make a new file, so it's clear that the
                # modified time has changed.
                baz_t = modified(ps.qux / "foo" / "baz.txt")
                sleep(1)
                write(ps.foo / "test.txt", "New src")
                sync(ps.foo, ps.qux / "foo")
                @test exists(ps.qux / "foo" / "test.txt")
                @test read(ps.qux / "foo" / "test.txt", String) == "New src"
                @test modified(ps.qux / "foo" / "baz.txt") == baz_t
                @test modified(ps.qux / "foo" / "test.txt") > baz_t
            end

            @testset "new destination" begin
                # Newer file of the same size is likely the result of an upload which
                # will always have a newer last modified time.
                test_t = modified(ps.foo / "test.txt")
                sleep(1)
                write(ps.qux / "foo" / "test.txt", "New dst")
                @test modified(ps.qux / "foo" / "test.txt") > test_t
                sync(ps.foo, ps.qux / "foo")
                @test read(ps.qux / "foo" / "test.txt", String) == "New dst"
                @test modified(ps.qux / "foo" / "test.txt") > test_t
            end

            @testset "no delete" begin
                # Test not deleting a file on sync
                rm(ps.foo / "test.txt")
                sync(ps.foo, ps.qux / "foo")
                @test exists(ps.qux / "foo" / "test.txt")
            end

            @testset "delete" begin
                # Test passing delete flag
                sync(ps.foo, ps.qux / "foo"; delete=true)
                @test !exists(ps.qux / "foo" / "test.txt")
                rm(ps.qux / "foo"; recursive=true)
            end

            @testset "mixed types" begin
                @testset "directory -> file" begin
                    @test_throws ArgumentError sync(ps.foo, ps.quux)
                end

                @testset "file -> directory" begin
                    @test_throws ArgumentError sync(ps.quux, ps.foo)
                end
            end

            @testset "walkpath order" begin
                # Test a condtion where the index could reorder the walkpath order.
                tmp_src = ps.root / "tmp-src"
                mkdir(tmp_src)
                src_file = tmp_src / "file1"
                write(src_file, "Hello World!")

                src_folder = tmp_src / "folder1"
                mkdir(src_folder)
                src_folder_file = src_folder / "file2"
                write(src_folder_file, "") # empty file

                src_folder2 = src_folder / "folder2"  # nested folders
                mkdir(src_folder2)
                src_folder2_file = src_folder2 / "file3"
                write(src_folder2_file, "Test")

                tmp_dst = ps.root / "tmp_dst"
                mkdir(tmp_dst)
                sync(tmp_src, tmp_dst)
                @test exists(tmp_dst / "folder1" / "folder2" / "file3")
                rm(tmp_src; recursive=true)
                rm(tmp_dst; recursive=true)
            end

            @testset "non-existent destination parent" begin
                # Test consistency with `cp` (e.g., destination parent must exist)
                # See full `cp` comparison tests above
                dest = ps.root / "non-existent" / "destination"

                # TODO: Use a more specific error type
                @test_throws ErrorException sync(ps.foo, dest)
            end
        end
    end

    function test_symlink(ps::PathSet)
        if ps.link
            @testset "symlink" begin
                @test_throws ErrorException symlink(ps.foo, ps.plugh)
                symlink(ps.foo, ps.plugh; exist_ok=true, overwrite=true)
                symlink(ps.foo, ps.plugh; exist_ok=true)
                @test_throws ErrorException symlink(ps.foo / "thud", ps.plugh; exist_ok=true, overwrite=true)
            end
        end
    end

    function test_touch(ps::PathSet)
        @testset "touch" begin
            newfile = ps.root / "newfile"
            touch(newfile)
            @test exists(newfile)
            @test ispath(newfile)
            rm(newfile)
        end
    end

    function test_tmpname(ps::PathSet)
        @testset "tmpname" begin
            @test isa(tmpname(), AbstractPath)
            @test hasparent(tmpname())
            @test exists(parent(tmpname()))
        end
    end

    function test_tmpdir(ps::PathSet)
        @testset "tmpname" begin
            @test isa(tmpdir(), AbstractPath)
            @test exists(tmpdir())
            @test isdir(tmpdir())
        end
    end

    function test_mktmp(ps::PathSet)
        @testset "mktmp" begin
            mktmp(ps.root) do path, io
                @test exists(path)
                @test iswritable(io)
                write(io, "Foobar")
                seekstart(io)
                @test read(io, String) == "Foobar"
            end
        end
    end

    function test_mktmpdir(ps::PathSet)
        @testset "mktmpdir" begin
            mktmpdir(ps.root) do path
                @test exists(path)
                write(path / "test.txt", "Foobar")
                @test read(path / "test.txt", String) == "Foobar"
            end
        end
    end

    function test_chown(ps::PathSet)
        @testset "chown" begin
            newfile = ps.root / "newfile"
            touch(newfile)

            if haskey(ENV, "USER")
                if ENV["USER"] == "root"
                    chown(newfile, "nobody", "nogroup"; recursive=true)
                elseif VERSION >= v"1.2"    # Error type changed in Julia 1.2
                    @test_throws ProcessFailedException chown(newfile, "nobody", "nogroup"; recursive=true)
                else
                    @test_throws ErrorException chown(newfile, "nobody", "nogroup"; recursive=true)
                end
            end

            rm(newfile)
        end
    end

    function test_chmod(ps::PathSet)
        @testset "chmod" begin
            newfile = ps.root / "newfile"
            newpath = ps.root / "thud"

            touch(newfile)
            mkdir(newpath)
            chmod(newfile, user=(READ+WRITE+EXEC), group=(READ+EXEC), other=READ)
            @test string(mode(newfile)) == "-rwxr-xr--"
            @test isexecutable(newfile)
            @test iswritable(newfile)
            @test isreadable(newfile)

            chmod(newfile, "-x")
            @test !isexecutable(newfile)

            @test string(mode(newfile)) == "-rw-r--r--"
            chmod(newfile, "+x")
            write(newfile, "foobar")
            @test read(newfile, String) == "foobar"
            chmod(newfile, "u=rwx")

            open(newfile, "r") do io
                @test read(io, String) == "foobar"
            end

            chmod(newpath, mode(newfile); recursive=true)
        end
    end

    function test_download(ps::PathSet)
        @testset "download" begin
            rm(ps.foo / "README.md"; force=true)
            download(
                "https://github.com/rofinn/FilePathsBase.jl/blob/master/README.md",
                ps.foo / "README.md"
            )
            @test exists(ps.foo / "README.md")

            # Test downloading from another path
            download(ps.foo / "README.md", ps.qux / "README.md")
            @test exists(ps.qux / "README.md")

             # Test downloading to a string
            download(ps.foo / "README.md", string(ps.fred / "README.md"))
            @test exists(ps.fred / "README.md")
            rm.([ps.foo / "README.md", ps.qux / "README.md", ps.fred / "README.md"])
        end
    end

    TESTALL = [
        test_registration,
        test_show,
        test_cmd,
        test_parse,
        test_convert,
        test_components,
        test_indexing,
        test_iteration,
        test_parents,
        test_descendants_and_ascendants,
        test_join,
        test_splitext,
        test_basename,
        test_filename,
        test_extensions,
        test_isempty,
        test_normalize,
        test_canonicalize,
        test_relative,
        test_absolute,
        test_isdir,
        test_isfile,
        test_stat,
        test_filesize,
        test_modified,
        test_created,
        test_issocket,
        test_isfifo,
        test_ischardev,
        test_isblockdev,
        test_ismount,
        test_isexecutable,
        test_isreadable,
        test_iswritable,
        test_cd,
        test_readpath,
        test_walkpath,
        test_read,
        test_write,
        test_mkdir,
        test_cp,
        test_mv,
        test_symlink,
        test_touch,
        test_tmpname,
        test_tmpdir,
        test_mktmp,
        test_mktmpdir,
        test_chown,
        test_chmod,
        test_download,
    ]

    function test(ps::PathSet, test_sets=TESTALL)
        try
            initialize(ps)

            for ts in test_sets
                ts(ps)
            end
        finally
            rm(ps.root; recursive=true, force=true)
        end
    end
end


text:
using SeisCore
using Test

@testset "SeisCore.jl" begin
    # Write your tests here.
end

ext: jl


text:
module Guide

using Color
using Compose
using Gadfly
using JSON

import Gadfly: render, escape_id, default_statistic

# Where the guide should be placed in relation to the plot.
abstract GuidePosition
immutable TopGuidePosition    <: GuidePosition end
immutable RightGuidePosition  <: GuidePosition end
immutable BottomGuidePosition <: GuidePosition end
immutable LeftGuidePosition   <: GuidePosition end
immutable UnderGuidePosition  <: GuidePosition end
immutable OverGuidePosition   <: GuidePosition end

const top_guide_position    = TopGuidePosition()
const right_guide_position  = RightGuidePosition()
const bottom_guide_position = BottomGuidePosition()
const left_guide_position   = LeftGuidePosition()
const under_guide_position  = UnderGuidePosition()
const over_guide_position   = OverGuidePosition()

immutable PanelBackground <: Gadfly.GuideElement
end

const background = PanelBackground

function render(guide::PanelBackground, theme::Gadfly.Theme,
                aess::Vector{Gadfly.Aesthetics})
    back = compose(canvas(order=-1),
                   rectangle(),
                   svgclass("guide background"),
                   stroke(theme.panel_stroke),
                   fill(theme.panel_fill),
                   opacity(theme.panel_opacity))

    {(back, under_guide_position)}
end

immutable ZoomSlider <: Gadfly.GuideElement
end

const zoomslider = ZoomSlider

function render(guide::ZoomSlider, theme::Gadfly.Theme,
                aess::Vector{Gadfly.Aesthetics})

    edge_pad = 3mm
    slide_pad = 0.5mm
    button_size = 4mm
    slider_size = 20mm
    background_color = "#eaeaea"
    foreground_color = "#6a6a6a"
    #highlight_color = string("#", hex(theme.highlight_color(color(foreground_color))))
    highlight_color = "#cd5c5c";

    minus_button = compose(canvas(1w - edge_pad - 2*button_size - slider_size,
                                  edge_pad, button_size, button_size),
                           rectangle(),
                             stroke(foreground_color),
                             strokeopacity(0.0),
                             linewidth(0.3mm),
                           (polygon((0.2, 0.4), (0.8, 0.4),
                                    (0.8, 0.6), (0.2, 0.6)),
                            fill(foreground_color),
                            svgclass("button_logo")),
                           fill(background_color),
                           d3embed(
                               """
                               .on("click", zoomout_behavior(ctx))
                               .on("dblclick", function() { d3.event.stopPropagation(); })
                               .on("mouseover", zoomslider_button_mouseover("$(highlight_color)"))
                               .on("mouseout", zoomslider_button_mouseover("$(foreground_color)"))
                               """))

    slider_width = 2mm
    slider_xpos = 1w - edge_pad - button_size - slider_size + slide_pad

    slider_min_pos = slider_xpos + slider_width/2
    slider_max_pos = slider_xpos + slider_size - 2*slide_pad - slider_width/2

    slider = compose(canvas(slider_xpos,
                            edge_pad, slider_size - 2 * slide_pad, button_size),
                     (rectangle(),
                      fill(background_color),
                      d3embed(".on(\"click\", zoomslider_track_behavior(ctx, %x, %x))",
                              slider_min_pos, slider_max_pos)),
                     (rectangle(0.5cx - slider_width/2, 0.0, slider_width, 1h),
                      fill(foreground_color),
                      svgclass("zoomslider_thumb"),
                      d3embed(
                        """
                        .call(zoomslider_behavior(ctx, %x, %x))
                        .on("mouseover", zoomslider_thumb_mouseover("$(highlight_color)"))
                        .on("mouseout", zoomslider_thumb_mouseover("$(foreground_color)"))
                        """,
                        slider_min_pos, slider_max_pos)))

    plus_button = compose(canvas(1w - edge_pad - button_size, edge_pad,
                                    button_size, button_size),
                          rectangle(),
                            stroke(foreground_color),
                            strokeopacity(0.0),
                            linewidth(0.3mm),
                          (polygon((0.2, 0.4), (0.4, 0.4), (0.4, 0.2),
                                   (0.6, 0.2), (0.6, 0.4), (0.8, 0.4),
                                   (0.8, 0.6), (0.6, 0.6), (0.6, 0.8),
                                   (0.4, 0.8), (0.4, 0.6), (0.2, 0.6)),
                           fill(foreground_color),
                           svgclass("button_logo")),
                          fill(background_color),
                          d3embed(
                              """
                              .on("click", zoomin_behavior(ctx))
                              .on("dblclick", function() { d3.event.stopPropagation(); })
                              .on("mouseover", zoomslider_button_mouseover("$(highlight_color)"))
                              .on("mouseout", zoomslider_button_mouseover("$(foreground_color)"))
                              """))

    root = compose(canvas(d3only=true),
                   minus_button,
                   slider,
                   plus_button,
                   stroke(nothing),
                   #stroke(foreground_color),
                   svgclass("guide zoomslider"),
                   opacity(0.0))

    {(root, over_guide_position)}
end

immutable ColorKey <: Gadfly.GuideElement
    title::Union(String, Nothing)

    function ColorKey(title=nothing)
        new(title)
    end
end

const colorkey = ColorKey

# A helper for render(::ColorKey) for rendering guides for discrete color
# scales.
function render_discrete_color_key(colors::Vector{ColorValue},
                                   labels::Dict{ColorValue, String},
                                   title_canvas::Canvas,
                                   title_width::Measure,
                                   theme::Gadfly.Theme)
    # Key entries
    n = length(colors)

    entry_width, entry_height = text_extents(theme.key_label_font,
                                             theme.key_label_font_size,
                                             values(labels)...)
    entry_width += entry_height # make space for the color swatch

    # Rewrite to put toggleable things in a group.
    swatch_padding = 1mm
    swatch_size = 1cy - swatch_padding
    swatch_canvas = canvas(0w, 0h + title_canvas.box.height,
                           1w, n * (entry_height + swatch_padding),
                           unit_box=UnitBox(0, 0, 1, n))
    for (i, c) in enumerate(colors)
        if theme.colorkey_swatch_shape == :square
            swatch_shape = rectangle(0, i - 1, swatch_size, swatch_size)
        elseif theme.colorkey_swatch_shape == :circle
            swatch_shape = circle(0.5cy, (i - 1)cy + entry_height/2, swatch_size/2)
        end

        swatch_shape = compose(swatch_shape,
                               fill(c),
                               stroke(theme.highlight_color(c)),
                               linewidth(theme.highlight_width))

        label = labels[c]
        swatch_label = compose(text(1cy, (i - 1)cy + entry_height/2,
                                    label, hleft, vcenter),
                               stroke(nothing),
                               fill(theme.key_label_color))

        color_class = @sprintf("color_%s", escape_id(label))
        swatch = compose(combine(swatch_shape, swatch_label),
                         svgclass(@sprintf("guide %s", color_class)),
                         d3embed(@sprintf(
                            ".on(\"click\", guide_toggle_color(\"%s\"))",
                            color_class)))
        swatch_canvas = compose(swatch_canvas, swatch)
    end

    swatch_canvas = compose(swatch_canvas,
                            font(theme.key_label_font),
                            fontsize(theme.key_label_font_size))

    title_canvas_pos = theme.guide_title_position == :left ?
        entry_height + swatch_padding : 0
    title_canvas = compose(canvas(title_canvas_pos, 0h, 1w,
                                  title_canvas.box.height),
                           title_canvas)

    compose(canvas(0, 0, max(title_width, entry_width) + 3swatch_padding,
                   swatch_canvas.box.height + title_canvas.box.height, order=2),
            pad(compose(canvas(), swatch_canvas, title_canvas), 2mm))
end

# A helper for render(::ColorKey) for rendering guides for continuous color
# scales.
function render_continuous_color_key(colors::Vector{ColorValue},
                                     labels::Dict{ColorValue, String},
                                     title_canvas::Canvas,
                                     title_width::Measure,
                                     theme::Gadfly.Theme)

    # Key entries
    entry_width, entry_height = text_extents(theme.key_label_font,
                                             theme.key_label_font_size,
                                             values(labels)...)
    entry_width += entry_height # make space for the color swatch

    unlabeled_swatches = 0
    for c in colors
        if labels[c] == ""
            unlabeled_swatches += 1
        end
    end

    unlabeled_swatch_height = 1.0mm
    swatch_padding = 1mm

    swatch_canvas = canvas(0w, 0h + title_canvas.box.height, 1w,
                           unlabeled_swatches * unlabeled_swatch_height +
                           (length(colors) - unlabeled_swatches) * entry_height)

    # Nudge things to overlap slightly avoiding any gaps.
    nudge = 0.1mm

    y = 0cy
    for (i, c) in enumerate(colors)
        if labels[c] == ""
            swatch_square = compose(rectangle(0, y,
                                              entry_height,
                                              unlabeled_swatch_height + nudge),
                                    fill(c),
                                    linewidth(theme.highlight_width))

            swatch_canvas = compose(swatch_canvas, swatch_square)

            y += unlabeled_swatch_height
        else
            swatch_square = compose(rectangle(0, y,
                                              entry_height,
                                              entry_height + nudge),
                                    fill(c),
                                    linewidth(theme.highlight_width))
            swatch_label = compose(text(entry_height + swatch_padding,
                                        y + entry_height / 2,
                                        labels[c],
                                        hleft, vcenter),
                                   fill(theme.key_label_color))

            swatch_canvas = compose(swatch_canvas, swatch_square, swatch_label)

            y += entry_height
        end
    end

    swatch_canvas = compose(swatch_canvas,
                            font(theme.key_label_font),
                            fontsize(theme.key_label_font_size),
                            stroke(nothing))

    title_canvas_pos = theme.guide_title_position == :left ?
        entry_height + swatch_padding : 0
    title_canvas = compose(canvas(title_canvas_pos, 0h, 1w,
                                  title_canvas.box.height),
                           title_canvas)

    compose(canvas(0, 0, max(title_width, entry_width) + 3swatch_padding,
                   swatch_canvas.box.height + title_canvas.box.height, order=2),
            pad(compose(canvas(), swatch_canvas, title_canvas), 2mm))
end

function render(guide::ColorKey, theme::Gadfly.Theme,
                aess::Vector{Gadfly.Aesthetics})

    if theme.key_position == :none
        return nothing
    else
        used_colors = Set{ColorValue}()
        colors = Array(ColorValue, 0) # to preserve ordering
        labels = Dict{ColorValue, Set{String}}()

        continuous_guide = false
        guide_title = guide.title

        for aes in aess
            if guide_title === nothing && !is(aes.color_key_title, nothing)
                guide_title = aes.color_key_title
            end

            if aes.color_key_colors === nothing
                continue
            end

            if !is(aes.color_key_continuous, nothing) && aes.color_key_continuous
                continuous_guide = true
            end

            color_key_labels = aes.color_label(aes.color_key_colors)
            for (color, label) in zip(aes.color_key_colors, color_key_labels)
                if !in(color, used_colors)
                    push!(used_colors, color)
                    push!(colors, color)
                    labels[color] = Set{String}()
                    push!(labels[color], label)
                else
                    push!(labels[color], label)
                end
            end
        end

        if guide_title === nothing
            guide_title = "Color"
        end

        pretty_labels = Dict{ColorValue, String}()
        for (color, label) in labels
            pretty_labels[color] = join(labels[color], ", ")
        end

        # Key title
        title_width, title_height = text_extents(theme.key_title_font,
                                             theme.key_title_font_size,
                                             guide_title)

        if theme.guide_title_position == :left
            title_form = text(0.0w, title_height, guide_title, hleft, vbottom)
        elseif theme.guide_title_position == :center
            title_form = text(0.5w, title_height, guide_title, hcenter, vbottom)
        elseif theme.guide_title_position == :right
            title_form = text(1.0w, title_height, guide_title, hright, vbottom)
        else
            error("$(theme.guide_title_position) is not a valid guide title position")
        end

        title_padding = 2mm
        title_canvas = compose(canvas(0w, 0h, 1w, title_height + title_padding),
                           title_form,
                           stroke(nothing),
                           font(theme.key_title_font),
                           fontsize(theme.key_title_font_size),
                           fill(theme.key_title_color))

        if theme.colorkey_swatch_shape != :circle &&
        theme.colorkey_swatch_shape != :square
            error("$(theme.colorkey_swatch_shape) is not a valid color key swatch shape")
        end

        if continuous_guide
            c = render_continuous_color_key(colors, pretty_labels, title_canvas,
                                        title_width, theme)
        else
            c = render_discrete_color_key(colors, pretty_labels, title_canvas,
                                      title_width, theme)
        end

        position = right_guide_position
        if theme.key_position == :left
            position = left_guide_position
        elseif theme.key_position == :right
            position = right_guide_position
        elseif theme.key_position == :top
            position = top_guide_position
        elseif theme.key_position == :bottom
            position = bottom_guide_position
        end

        return {(c, position)}
    end
end

immutable XTicks <: Gadfly.GuideElement
    label::Bool
    ticks::Union(Nothing, AbstractArray)

    function XTicks(; label::Bool=true,
                      ticks::Union(Nothing, AbstractArray)=nothing)
        new(label, ticks)
    end
end

const xticks = XTicks

function default_statistic(guide::XTicks)
    Stat.xticks(guide.ticks)
end

function render(guide::XTicks, theme::Gadfly.Theme,
                aess::Vector{Gadfly.Aesthetics})
    ticks = Dict()
    grids = Set()

    for aes in aess
        if Gadfly.issomething(aes.xtick)
            for (val, label) in zip(aes.xtick, aes.xtick_label(aes.xtick))
                ticks[val] = label
            end
        end

        if Gadfly.issomething(aes.xgrid)
            for val in aes.xgrid
                push!(grids, val)
            end
        end
    end

    viewmin, viewmax = nothing, nothing
    for aes in aess
        if (viewmin != nothing && aes.xviewmin != nothing && aes.xviewmin < viewmin) ||
           (viewmin == nothing && aes.xviewmin != nothing)
            viewmin = aes.xviewmin
        end
        if (viewmax != nothing && aes.xviewmax != nothing && aes.xviewmax > viewmax) ||
           (viewmax == nothing && aes.xviewmax != nothing)
            viewmax = aes.xviewmax
        end
    end

    # grid lines
    grid_lines = compose(canvas(),
                         [lines((t, 0h), (t, 1h)) for t in grids]...,
                         stroke(theme.grid_color),
                         linewidth(theme.grid_line_width),
                         svgclass("guide xgridlines yfixed"))

    if !guide.label
        return {(grid_lines, under_guide_position)}
    end

    # tick labels
    (_, height) = text_extents(theme.minor_label_font,
                               theme.minor_label_font_size,
                               values(ticks)...)
    padding = 1mm

    texts = Array(Canvas, length(ticks))
    for (i, (tick, label)) in enumerate(ticks)
        txt = text(tick, 1h - padding, label, hcenter, vbottom)
        if (viewmin != nothing && viewmax != nothing) && viewmin <= tick <= viewmax
            texts[i] = compose(canvas(units_inherited=true), txt)
        else
            texts[i] = compose(canvas(units_inherited=true, d3only=true), txt,
                               d3embed(""".attr("visibility", "hidden")"""))
        end
    end

    tick_labels = compose(canvas(0, 0, 1w, height + 2padding, order=-1),
                          texts...,
                          stroke(nothing),
                          fill(theme.minor_label_color),
                          font(theme.minor_label_font),
                          fontsize(theme.minor_label_font_size),
                          svgclass("guide xlabels"))

    {(grid_lines, under_guide_position),
     (tick_labels, bottom_guide_position)}
end

immutable YTicks <: Gadfly.GuideElement
    label::Bool
    ticks::Union(Nothing, AbstractArray)

    function YTicks(; label::Bool=true,
                      ticks::Union(Nothing, AbstractArray)=nothing)
        new(label, ticks)
    end
end

const yticks = YTicks

function default_statistic(guide::YTicks)
    Stat.yticks(guide.ticks)
end

function render(guide::YTicks, theme::Gadfly.Theme,
                aess::Vector{Gadfly.Aesthetics})
    ticks = Dict()
    grids = Set()

    for aes in aess
        if Gadfly.issomething(aes.ytick)
            for (val, label) in zip(aes.ytick, aes.ytick_label(aes.ytick))
                ticks[val] = label
            end
        end

        if Gadfly.issomething(aes.ygrid)
            for val in aes.ygrid
                push!(grids, val)
            end
        end
    end

    viewmin, viewmax = nothing, nothing
    for aes in aess
        if (viewmin != nothing && aes.yviewmin != nothing && aes.yviewmin < viewmin) ||
           (viewmin == nothing && aes.yviewmin != nothing)
            viewmin = aes.yviewmin
        end
        if (viewmax != nothing && aes.yviewmax != nothing && aes.yviewmax > viewmax) ||
           (viewmax == nothing && aes.yviewmax != nothing)
            viewmax = aes.yviewmax
        end
    end

    # grid lines
    grid_lines = compose(canvas(),
                         [lines((0w, t), (1w, t)) for t in grids]...,
                         stroke(theme.grid_color),
                         linewidth(theme.grid_line_width),
                         svgclass("guide ygridlines xfixed"))

    if !guide.label
        return {(grid_lines, under_guide_position)}
    end

    # tick labels
    (width, _) = text_extents(theme.minor_label_font,
                              theme.minor_label_font_size,
                              values(ticks)...)
    padding = 1mm
    width += 2padding

    texts = Array(Canvas, length(ticks))
    for (i, (tick, label)) in enumerate(ticks)
        txt = text(width - padding, tick, label, hright, vcenter)
        if (viewmin != nothing && viewmax != nothing) && viewmin <= tick <= viewmax
            texts[i] = compose(canvas(units_inherited=true), txt)
        else
            texts[i] = compose(canvas(units_inherited=true, d3only=true), txt,
                               d3embed(""".attr("visibility", "hidden")"""))
        end
    end

    tick_labels = compose(canvas(0, 0, width, 1cy, order=-1),
                          texts...,
                          stroke(nothing),
                          fill(theme.minor_label_color),
                          font(theme.minor_label_font),
                          fontsize(theme.minor_label_font_size),
                          svgclass("guide ylabels"))

    {(grid_lines, under_guide_position),
     (tick_labels, left_guide_position)}
end

# X-axis label Guide
immutable XLabel <: Gadfly.GuideElement
    label::Union(Nothing, String)
end

const xlabel = XLabel

function render(guide::XLabel, theme::Gadfly.Theme,
                aess::Vector{Gadfly.Aesthetics})
    if guide.label === nothing || isempty(guide.label)
        return nothing
    end

    (_, text_height) = text_extents(theme.major_label_font,
                                    theme.major_label_font_size,
                                    guide.label)

    padding = 2mm
    c = compose(canvas(0, 0, 1w, text_height + 2padding),
                text(0.5w, 1h - padding, guide.label, hcenter, vbottom),
                stroke(nothing),
                fill(theme.major_label_color),
                font(theme.major_label_font),
                fontsize(theme.major_label_font_size))

    {(c, bottom_guide_position)}
end

# Y-axis label Guide
immutable YLabel <: Gadfly.GuideElement
    label::Union(Nothing, String)
end

const ylabel = YLabel

function render(guide::YLabel, theme::Gadfly.Theme, aess::Vector{Gadfly.Aesthetics})
    if guide.label === nothing || isempty(guide.label)
        return nothing
    end

    (text_width, text_height) = text_extents(theme.major_label_font,
                                             theme.major_label_font_size,
                                             guide.label)
    padding = 1mm
    c = compose(canvas(0, 0, text_height + 2padding, 1cy,
                       rotation=Rotation(-0.5pi, 0.5w, 0.5h)),
                text(0.5w, 0.5h, guide.label, hcenter, vcenter),
                stroke(nothing),
                fill(theme.major_label_color),
                font(theme.major_label_font),
                fontsize(theme.major_label_font_size))

    {(c, left_guide_position)}
end

# Title Guide
immutable Title <: Gadfly.GuideElement
    label::Union(Nothing, String)
end

const title = Title

function render(guide::Title, theme::Gadfly.Theme,
                aess::Vector{Gadfly.Aesthetics})
    if guide.label === nothing || isempty(guide.label)
        return nothing
    end

    (_, text_height) = text_extents(theme.major_label_font,
                                    theme.major_label_font_size,
                                    guide.label)

    padding = 2mm
    c = compose(canvas(0, 0, 1w, text_height + 2padding),
                text(0.5w, 1h - padding, guide.label, hcenter, vbottom),
                stroke(nothing),
                fill(theme.major_label_color),
                font(theme.major_label_font),
                fontsize(theme.major_label_font_size))

    {(c, top_guide_position)}
end

# Arrange a plot with its guides
#
# Args:
#   plot_canvas: A canvas containing the plot graphics.
#   guides: Tuples of guide canvases each with a GuidePosition giving
#           where the guide should be placed relative to the plot.
#
# Returns:
#   A new canvas containing the plot with guides layed out in the specified
#   manner.
function layout_guides(plot_canvas::Canvas,
                       theme::Gadfly.Theme,
                       guides::(Canvas, GuidePosition)...;
                       preserve_plot_canvas_size=false)
    # Every guide is updated to use the plot's unit box.
    guides = [(set_unit_box(guide, plot_canvas.unit_box), pos)
              for (guide, pos) in guides]

    # Group by position
    top_guides    = Canvas[]
    right_guides  = Canvas[]
    bottom_guides = Canvas[]
    left_guides   = Canvas[]
    under_guides  = Canvas[]
    over_guides   = Canvas[]
    for (guide, pos) in guides
        if pos === top_guide_position
            push!(top_guides, guide)
        elseif pos === right_guide_position
            push!(right_guides, guide)
        elseif pos === bottom_guide_position
            push!(bottom_guides, guide)
        elseif pos === left_guide_position
            push!(left_guides, guide)
        elseif pos === under_guide_position
            push!(under_guides, guide)
        elseif pos === over_guide_position
            push!(over_guides, guide)
        end
    end

    # Since top/right/bottom/left guides are drawn without overlaps, we use the
    # canvas z-order to determine ordering, with lowest ordered guides placed
    # nearest to the panel.
    canvas_order = canvas -> canvas.order
    rev_canvas_order = canvas -> -canvas.order
    sort!(by=canvas_order,     top_guides)
    sort!(by=canvas_order,     right_guides)
    sort!(by=canvas_order,     bottom_guides)
    sort!(by=rev_canvas_order, left_guides)

    # Stack the guides on edge edge of the plot

    top_guides    = vstack(0, 0, 1, [(g, hcenter) for g in top_guides]...)
    right_guides  = hstack(0, 0, 1, [(g, vcenter) for g in right_guides]...)
    bottom_guides = vstack(0, 0, 1, [(g, hcenter) for g in bottom_guides]...)
    left_guides   = hstack(0, 0, 1, [(g, vcenter) for g in left_guides]...)

    # Reposition each guide stack, now that we know the extents
    t = top_guides.box.height
    r = right_guides.box.width
    b = bottom_guides.box.height
    l = left_guides.box.width

    pw = 1cx - l - r # plot width
    ph = 1cy - t - b # plot height

    top_guides    = compose(set_box(top_guides,    BoundingBox(l, 0, pw, t)))
        # TODO: clip path

    right_guides  = compose(set_box(right_guides,  BoundingBox(l + pw, t, r, ph)))
        # TODO: clip path

    clippad_top    = 4mm
    clippad_bottom = 1mm
    clippad_left   = 0.1w
    clippad_right  = 0.1w

    bottom_guides = compose(set_box(bottom_guides, BoundingBox(l, t + ph, pw, b)))
    left_guides = compose(set_box(left_guides, BoundingBox(0, t, l, ph)))

    root_canvas = preserve_plot_canvas_size ?
        canvas(0, 0, 1.0w + l + r, 1.0h + t + b) : canvas()

    compose(root_canvas,
            (canvas(l, t, pw, ph),
                {canvas(units_inherited=true, order=-1, clip=true), under_guides...},
                {canvas(units_inherited=true, order=1000, clip=true), over_guides...},
                (canvas(units_inherited=true, order=1, clip=true),  plot_canvas),
                d3embed(@sprintf(
                    ".on(\"mouseover\", guide_background_mouseover(%s))",
                    json(theme.highlight_color(theme.grid_color)))),
                d3embed(@sprintf(
                    ".on(\"mouseout\", guide_background_mouseout(%s))",
                    json(theme.grid_color))),
                isempty(under_guides) ?
                    nothing : d3embed(
                        """
                        .call(zoom_behavior(ctx))
                        """)),
            top_guides, right_guides, bottom_guides, left_guides)
end

end # module Guide


text:
module WebPlotDigitizer

using JSON
using DataStructures
using Tar
using Dates

export sortby, sortby!
import Base: getindex, show, size, axes, to_index

abstract type Axes{T} end

getindex(ax::Axes,name::String) = ax.data[name]

struct XYAxes{T} <: Axes{T}
    isLogX::Bool
    isLogY::Bool
    data::OrderedDict{String,<:Matrix{T}}
end

struct BarAxes{T} <: Axes{T}
    isRotated::Bool
    isLog::Bool
    data::OrderedDict{String,<:Matrix{T}}
end

struct PolarAxes{T} <: Axes{T}
    isClockwise::Bool
    isLog::Bool
    isDegrees::Bool
    data::OrderedDict{String,<:Matrix{T}}
end

Axis(ax::Dict,data::Dict) = Axis(Val(Symbol(ax["type"])),ax,data)
Axis(::Val{T},args...) where T = error("Axis types XYAxes known. Got: $T")

function Axis(::Val{:XYAxes},ax,data)
    XYAxes(ax["isLogX"],ax["isLogY"],
        OrderedDict([d["name"] => convert_values_entry(d["data"],ax)
                for d in data["datasetColl"] if d["axesName"] == ax["name"]]))
end

function Axis(::Val{:BarAxes},ax,data)
    BarAxes(ax["isRotated"],ax["isLog"],
        OrderedDict([d["name"] => convert_values_entry(d["data"],ax)
                for d in data["datasetColl"] if d["axesName"] == ax["name"]]))
end

function Axis(::Val{:PolarAxes},ax,data)
    PolarAxes(ax["isClockwise"],ax["isLog"],ax["isDegrees"],
        OrderedDict([d["name"] => convert_values_entry(d["data"],ax)
                for d in data["datasetColl"] if d["axesName"] == ax["name"]]))
end

const re_full_datetime = r"^\d{4}\/\d{1,2}\/\d{1,2} \d{1,2}:\d{1,2}$"
const re_full_date     = r"^\d{4}\/\d{1,2}\/\d{1,2}$"
const re_year_month    = r"^\d{4}\/\d{1,2}$"
const re_time          = r"^\d{1,2}:\d{1,2}$"
const re_datetime = [re_full_datetime, re_full_date, re_year_month, re_time]

function parse_import_value(val::Number, dx::String)
    if any(x->occursin(x,dx), re_datetime)
        if occursin(re_time,dx) # time only
            return Time(Dates.unix2datetime(val/1000))
        elseif !(occursin(":",dx))
            return Date(round(Dates.unix2datetime(val/1000),Dates.Day))
        end
        return DateTime(Dates.unix2datetime(val/1000))
    elseif tryparse(Float64,dx) != nothing
        return float(val)
    end
    error("Expected to parse a date or time value. Got:$dx")
end

parse_import_value(val::Number, dx::Number) = float(val)

# Convert to supported types.
# Always store a 2D array.
const column_names = ("dx", "dy", "dz")
convert_values_entry(values,ax) = mapfoldr(vcat,values) do p
    col_specification = getindex.(Ref(ax["calibrationPoints"][1]),column_names)[1:length(p["value"])]
    permutedims(parse_import_value.(p["value"], col_specification))
end

struct WPDProject
    axes::OrderedDict{String,Axes}
end

getindex(wpd::WPDProject,name::String) = wpd.axes[name]

function load_from_json(path)
    data = JSON.parse(read(path,String))
    return WPDProject(OrderedDict([
        ax["name"] => Axis(ax,data)
    for ax in data["axesColl"]]))
end

function isWPDProject(path,name)
    content = getfield.(Tar.list(path),:path)
    "$name/info.json" in content
end

function getWPDProjectName(path)
    content = filter(Tar.list(path)) do f
        f.type == :directory
    end
    length(content) != 1 && error("Can't retrive project name. There are multiple folders at toplevel, a WebPlotDigitizer project only has one.")
    return dirname(content[1].path)
end

function getWPDProjectFile(wpd_folder)
    info = JSON.parse(read(joinpath(wpd_folder, "info.json"),String))
    return info["json"]
end

function load_from_tar(path)
    name = getWPDProjectName(path)
    isWPDProject(path,name) || error("Tar at '$path' is not a WebPlotDigitizer project.")
    tmp_dir = Tar.extract(path)
    wpd_folder = joinpath(tmp_dir, name)
    wpd_file_name = getWPDProjectFile(wpd_folder)
    wpd_file = joinpath(tmp_dir, name, wpd_file_name)
    wpd = load_from_json(wpd_file)
    rm(tmp_dir,recursive=true)
    return wpd
end

const load_project = load_from_tar
const load_json = load_from_json

getaxisdescription(::T) where T = split("$T",".")[end]

function show(io::IO,wpd::WPDProject)
    axes = keys(wpd.axes)
    print(io,"WebPlotDigitizer:")
    for axis in keys(wpd.axes)
        println(io)
        print(io," '$axis' $(getaxisdescription(wpd[axis]))")
        for dataset in keys(wpd[axis].data)
            println(io)
            print(io,"   $dataset")
        end
    end
end

function sortby(ds::Matrix,by::Int;rev::Bool=false)
    idx = to_index(ds,by)
    return sortslices(ds,by=x->x[idx],dims=1,rev=rev)
end

function sortby!(ds::Matrix,by::Int;rev::Bool=false)
    sorted = sortby(ds,by,rev=rev)
    ds .= sorted
end

function sortby!(ax::Axes, by::Int; rev::Bool=false)
    for ds in ax.data
        sortby!(ds[2], by, rev=rev)
    end
end

end


text:
# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).
# Since these FMAs can increase the performance of many numerical algorithms,
# we need to opt-in explicitly.
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
@muladd begin

# Visualize a single variable in a 2D plot (default: heatmap)
#
# Note: This is an experimental feature and may be changed in future releases without notice.
RecipesBase.@recipe function f(pds::PlotDataSeries{<:AbstractPlotData{2}})
  @unpack plot_data, variable_id = pds
  @unpack x, y, data, variable_names, orientation_x, orientation_y = plot_data

  # Set geometric properties
  xlims --> (x[begin], x[end])
  ylims --> (y[begin], y[end])
  aspect_ratio --> :equal

  # Set annotation properties
  legend -->  :none
  title --> variable_names[variable_id]
  colorbar --> :true
  xguide --> _get_guide(orientation_x)
  yguide --> _get_guide(orientation_y)

  # Set series properties
  seriestype --> :heatmap

  # Return data for plotting
  x, y, data[variable_id]
end

# Visualize a single variable in a 2D plot. Only works for `scatter` right now.
#
# Note: This is an experimental feature and may be changed in future releases without notice.
RecipesBase.@recipe function f(pds::PlotDataSeries{<:PlotData2D{<:Any, <:AbstractVector{<:AbstractVector}}})
  @unpack plot_data, variable_id = pds
  @unpack x, y, data, variable_names, orientation_x, orientation_y = plot_data

  # Set geometric properties
  xlims --> (minimum(x), maximum(x))
  ylims --> (minimum(y), maximum(y))
  aspect_ratio --> :equal

  # Set annotation properties
  legend -->  :none
  title --> variable_names[variable_id]
  colorbar --> :true
  xguide --> _get_guide(orientation_x)
  yguide --> _get_guide(orientation_y)

  # Set series properties
  seriestype --> :scatter
  markerstrokewidth --> 0

  marker_z --> data[variable_id]

  # Return data for plotting
  x, y
end

# Visualize the mesh in a 2D plot
#
# Note: This is an experimental feature and may be changed in future releases without notice.
RecipesBase.@recipe function f(pm::PlotMesh{<:AbstractPlotData{2}})
  @unpack plot_data = pm
  @unpack x, y, mesh_vertices_x, mesh_vertices_y = plot_data

  # Set geometric and annotation properties
  xlims --> (x[begin], x[end])
  ylims --> (y[begin], y[end])
  aspect_ratio --> :equal
  legend -->  :none
  grid --> false

  # Set series properties
  seriestype --> :path
  linecolor --> :grey
  linewidth --> 1

  # Return data for plotting
  mesh_vertices_x, mesh_vertices_y
end

# Visualize the mesh in a 2D plot
#
# Note: This is an experimental feature and may be changed in future releases without notice.
RecipesBase.@recipe function f(pm::PlotMesh{<:PlotData2D{<:Any, <:AbstractVector{<:AbstractVector}}})
  @unpack plot_data = pm
  @unpack x, y, mesh_vertices_x, mesh_vertices_y = plot_data

  # Set geometric and annotation properties
  xlims --> (minimum(x), maximum(x))
  ylims --> (minimum(y), maximum(y))
  aspect_ratio --> :equal
  legend -->  :none
  grid --> false

  # Set series properties
  seriestype --> :path
  linecolor --> :grey
  linewidth --> 1

  # Return data for plotting
  mesh_vertices_x, mesh_vertices_y
end

# Plot all available variables at once for convenience
#
# Note: This is an experimental feature and may be changed in future releases without notice.
RecipesBase.@recipe function f(pd::AbstractPlotData)
  # Create layout that is as square as possible, when there are more than 3 subplots.
  # This is done with a preference for more columns than rows if not.

  if length(pd) <= 3
    cols = length(pd)
    rows = 1
  else
    cols = ceil(Int, sqrt(length(pd)))
    rows = ceil(Int, length(pd)/cols)
  end

  layout := (rows, cols)

  # Plot all existing variables
  for (i, (variable_name, series)) in enumerate(pd)
    @series begin
      subplot := i
      series
    end
  end

  # Fill remaining subplots with empty plot
  for i in (length(pd)+1):(rows*cols)
    @series begin
      subplot := i
      axis := false
      ticks := false
      legend := false
      [], []
    end
  end
end

# Plot a single variable.
RecipesBase.@recipe function f(pds::PlotDataSeries{<:AbstractPlotData{1}})
  @unpack plot_data, variable_id = pds
  @unpack x, data, variable_names, orientation_x = plot_data

  # Set geometric properties
  xlims --> (x[begin], x[end])

  # Set annotation properties
  legend --> :none
  title --> variable_names[variable_id]
  xguide --> _get_guide(orientation_x)

  # Return data for plotting
  x, data[:, variable_id]
end

# Plot the mesh as vertical lines from a PlotMesh object.
RecipesBase.@recipe function f(pm::PlotMesh{<:AbstractPlotData{1}})
  @unpack plot_data = pm
  @unpack x, mesh_vertices_x = plot_data

  # Set geometric and annotation properties
  xlims --> (x[begin], x[end])
  legend -->  :none

  # Set series properties
  seriestype --> :vline
  linecolor --> :grey
  linewidth --> 1

  # Return data for plotting
  mesh_vertices_x
end

# Create a plot directly from a TrixiODESolution for convenience
# The plot is created by a PlotData1D or PlotData2D object.
#
# Note: This is an experimental feature and may be changed in future releases without notice.
RecipesBase.@recipe function f(sol::TrixiODESolution)
  # Redirect everything to the recipe below
  return sol.u[end], sol.prob.p
end

# Note: If you change the defaults values here, you need to also change them in the PlotData1D or PlotData2D
#       constructor.
RecipesBase.@recipe function f(u, semi::AbstractSemidiscretization;
                               solution_variables=nothing,
                               grid_lines=true, max_supported_level=11, nvisnodes=nothing, slice=:xy,
                               point=(0.0, 0.0, 0.0), curve=nothing)
  # Create a PlotData1D or PlotData2D object depending on the dimension.
  if ndims(semi) == 1
    return PlotData1D(u, semi; solution_variables, nvisnodes, slice, point, curve)
  else
    return PlotData2D(u, semi;
                      solution_variables, grid_lines, max_supported_level,
                      nvisnodes, slice, point)
  end
end

# need to define this function because some keywords from the more general plot recipe
# are not supported (e.g., `max_supported_level`).
RecipesBase.@recipe function f(u, semi::DGMultiSemidiscretizationHyperbolic;
                               solution_variables=cons2cons, grid_lines=true)
  return PlotData2D(u, semi)
end

# Series recipe for UnstructuredPlotData2D
RecipesBase.@recipe function f(pds::PlotDataSeries{<:UnstructuredPlotData2D})

  pd = pds.plot_data
  @unpack variable_id = pds
  @unpack x, y, data, t, variable_names = pd

  # extract specific solution field to plot
  data_field = zeros(eltype(first(data)), size(data))
  for (i, data_i) in enumerate(data)
    data_field[i] = data_i[variable_id]
  end

  legend --> false
  aspect_ratio --> 1
  title --> pd.variable_names[variable_id]
  xlims --> extrema(x)
  ylims --> extrema(y)
  xguide --> _get_guide(1)
  yguide --> _get_guide(2)
  seriestype --> :heatmap
  colorbar --> :true

  return DGTriPseudocolor(global_plotting_triangulation_triplot((x, y), data_field, t)...)
end

# Visualize a 2D mesh given an `UnstructuredPlotData2D` object
@recipe function f(pm::PlotMesh{<:UnstructuredPlotData2D})
  pd = pm.plot_data
  @unpack x_face, y_face = pd

  # This line separates solution lines on each edge by NaNs to ensure that they are rendered
  # separately. The coordinates `xf`, `yf` and the solution `sol_f`` are assumed to be a matrix
  # whose columns correspond to different elements. We add NaN separators by appending a row of
  # NaNs to this matrix. We also flatten (e.g., apply `vec` to) the result, as this speeds up
  # plotting.
  x_face, y_face = map(x->vec(vcat(x, fill(NaN, 1, size(x, 2)))), (x_face, y_face))

  xlims --> extrema(x_face)
  ylims --> extrema(y_face)
  aspect_ratio --> :equal
  legend -->  :none

  # Set series properties
  seriestype --> :path
  linecolor --> :grey
  linewidth --> 1

  return x_face, y_face
end

RecipesBase.@recipe function f(cb::DiscreteCallback{<:Any, <:TimeSeriesCallback}, point_id::Integer)
  return cb.affect!, point_id
end

RecipesBase.@recipe function f(time_series_callback::TimeSeriesCallback, point_id::Integer)
  return PlotData1D(time_series_callback, point_id)
end

end # @muladd


text:
using TaylorIntegration, Test, DiffEqBase
using LinearAlgebra: norm

@testset "Testing `common.jl`" begin

    f(u,p,t) = u
    @testset "Test integration of ODE with numbers in common interface" begin
        u0 = 0.5
        tspan = (0.0, 1.0)
        prob = ODEProblem(f, u0, tspan)
        sol = solve(prob, TaylorMethod(50), abstol=1e-20)

        @test sol[end] - 0.5*exp(1) < 1e-12
    end

    f!(du, u, p, t) = (du .= u)
    @testset "Test integration of ODE with abstract arrays in common interface" begin
        u0 = rand(4, 2)
        tspan = (0.0, 1.0)
        prob = ODEProblem(f!, u0, tspan)
        sol = solve(prob, TaylorMethod(50), abstol=1e-20)

        @test norm(sol[end] - u0.*exp(1)) < 1e-12
    end

    tspan = (0.0,5.0)
    saveat_inputs = ([], 0:1:(tspan[2]+5), 0:1:tspan[2], 3:1:tspan[2], collect(0:1:tspan[2]))
    @testset "Test saveat behavior with numbers in common interface" begin
        u0 = 1.0
        prob = ODEProblem(f, u0, tspan)
        sol = solve(prob, TaylorMethod(20), abstol=1e-20)
        s = saveat_inputs[1]
        sol_taylor = solve(prob, TaylorMethod(20), abstol=1e-20, saveat = s)
        @test all(sol.t .== sol_taylor.t)
        @test all(sol_taylor.u .== sol.u)
        @test length(sol_taylor.t) == length(sol_taylor.u)
        s = saveat_inputs[2]
        sol_taylor = solve(prob, TaylorMethod(20), abstol=1e-20, saveat = s)
        @test sol_taylor.t == saveat_inputs[3]
        for s ∈ saveat_inputs[3:end]
            sol_taylor = solve(prob, TaylorMethod(20), abstol=1e-20, saveat = s)
            @test all(s .== sol_taylor.t)
            @test length(sol_taylor.t) == length(sol_taylor.u)
        end
    end

    @testset "Test saveat behavior with abstract arrays in common interface" begin
        u0 = rand(2)
        prob = ODEProblem(f!, u0, tspan)
        sol = solve(prob, TaylorMethod(20), abstol=1e-20)
        s = saveat_inputs[1]
        sol_taylor = solve(prob, TaylorMethod(20), abstol=1e-20, saveat = s)
        @test all(sol.t .== sol_taylor.t)
        @test all(sol_taylor.u .== sol.u)
        @test length(sol_taylor.t) == length(sol_taylor.u)
        s = saveat_inputs[2]
        sol_taylor = solve(prob, TaylorMethod(20), abstol=1e-20, saveat = s)
        @test sol_taylor.t == saveat_inputs[3]
        for s ∈ saveat_inputs[3:end]
            sol_taylor = solve(prob, TaylorMethod(20), abstol=1e-20, saveat = s)
            @test all(s .== sol_taylor.t)
            @test length(sol_taylor.t) == length(sol_taylor.u)
        end
    end

    @testset "Test throwing errors in common interface" begin
        u0 = rand(4, 2)
        tspan = (0.0, 1.0)
        prob1 = ODEProblem(f!, u0, tspan)
        sol = solve(prob1, TaylorMethod(50), abstol=1e-20)

        # `order` is not specified
        @test_throws ErrorException solve(prob, TaylorMethod(), abstol=1e-20)

        # Using a `callback`
        prob2 = ODEProblem(f!, u0, tspan, callback=nothing)
        @test_throws ErrorException solve(prob2, TaylorMethod(10), abstol=1e-20)
    end
end


text:
module Gemini

using WebSockets
using JSON
using Dates
using Base64
using Nettle
using HTTP
using StringEncodings

struct GeminiResponse
  status
  response
end

"""
Open a Websocket client to the v2/marketdata endpoint

https://docs.gemini.com/websocket-api/#market-data-version-2

# Arguments:
- `sandbox::Bool`: Sandbox request
- `channel::Channel{Dict}`: channel to pass data
- `names::Vector`: data feed name subscriptions (l2, candles_1m,...)
- `symbols::Vector`: symbol subscriptions (BTCUSD,...)
"""
function marketdata_v2(sandbox::Bool, channel::Channel{Dict}, names::Vector, symbols::Vector)::GeminiResponse
  if >(length(names), 0) && >(length(symbols), 0)
    if sandbox
      base_url = "wss://api.sandbox.gemini.com"
    else
      base_url = "wss://api.gemini.com"
    end
    msg = Dict(
      "type" => "subscribe",
      "subscriptions" => []
    )
    for name in names
      push!(
        msg["subscriptions"],
        Dict(
          "name" => name,
          "symbols" => symbols
        )
      )
    end
    WebSockets.open(base_url*"/v2/marketdata") do ws
      if isopen(ws)
        if writeguarded(ws, JSON.json(msg))
          while isopen(ws)
            data, success = readguarded(ws)
            if success
              put!(channel, JSON.parse(String(data))::Dict)
            end
          end
        else
          return GeminiResponse(false, Dict("error"=>"failed to send subscription information to Gemini"))
        end
      else
        return GeminiResponse(false, Dict("error"=>"failed to open websocket"))
      end
    end
  else
    return GeminiResponse(false, Dict("error"=>"no subscriptions given"))
  end
end

"""
Creates a new order

https://docs.gemini.com/rest-api/#new-order

# Arguments:
- `sandbox::Bool`: Sandbox request
- `api_key::String`: Gemini API key
- `api_secret::String`: Gemini API secret
- `side::String`: "buy" or "sell"
- `symbol::String`: The symbol for the new order
- `amount::String`: Quoted decimal amount to purchase
- `price::String`: Quoted decimal amount to spend per unit
- `type::String`: The order type. "exchange limit" for all order types except for stop-limit orders. "exchange stop limit" for stop-limit orders.
- `options::Vector{String}`:	Optional. An optional array containing at most one supported order execution option.
- `stop_price::String`: Optional. The price to trigger a stop-limit order. Only available for stop-limit orders.
"""
function new_order(sandbox::Bool, api_key::String, api_secret::String, side::String, symbol::String, amount::String, price::String, type::String, options::Vector{String}=Vector{String,1}(), stop_price::String="")
  if sandbox
    url = "https://api.sandbox.gemini.com/v1/order/new"
  else
    url = "https://api.gemini.com/v1/order/new"
  end
  payload = Dict(
    "request" => "/v1/order/new",
    "nonce" => string(Dates.datetime2epochms(Dates.now())),
    "symbol" => symbol,
    "amount" => amount,
    "price" => price,
    "side" => side,
    "type" => type,
    "options" => options,
    "stop_price" => stop_price
  )
  encoded_payload = encode(JSON.json(payload), "UTF-8")
  b64 = base64encode(encoded_payload)
  signature = hexdigest("SHA384", encode(api_secret, "UTF-8"), b64)
  request_headers = Dict(
    "Content-Type" => "text/plain",
    "Content-Length" => "0",
    "X-GEMINI-APIKEY" => api_key,
    "X-GEMINI-PAYLOAD" => b64,
    "X-GEMINI-SIGNATURE" => signature,
    "Cache-Control" => "no-cache"
  )
  try
    response = HTTP.request(
      "POST",
      url,
      request_headers;
      retry = false
    )
    return GeminiResponse(response.status, JSON.parse(String(response.body)))
  catch err
    if isa(err, HTTP.ExceptionRequest.StatusError)
      if ==(err.status, 503)
        return GeminiResponse(err.status, JSON.parse(String(err.response.body)))
      else
        return GeminiResponse(err.status, err.response.body)
      end
    end
  end
end

"""
Cancels an open order

https://docs.gemini.com/rest-api/#cancel-order

# Arguments:
- `sandbox::Bool`: Sandbox request
- `api_key::String`: Gemini API key
- `api_secret::String`: Gemini API secret
- `order_id::Int64`: Order id to cancel, given by new_order()
"""
function cancel_order(sandbox::Bool, api_key::String, api_secret::String, order_id::Int64)
  if sandbox
    url = "https://api.sandbox.gemini.com/v1/order/cancel"
  else
    url = "https://api.gemini.com/v1/order/cancel"
  end
  payload = Dict(
    "request" => "/v1/order/cancel",
    "nonce" => string(Dates.datetime2epochms(Dates.now())),
    "order_id" => order_id,
  )
  encoded_payload = encode(JSON.json(payload), "UTF-8")
  b64 = base64encode(encoded_payload)
  signature = hexdigest("SHA384", encode(api_secret, "UTF-8"), b64)
  request_headers = Dict(
    "Content-Type" => "text/plain",
    "Content-Length" => "0",
    "X-GEMINI-APIKEY" => api_key,
    "X-GEMINI-PAYLOAD" => b64,
    "X-GEMINI-SIGNATURE" => signature,
    "Cache-Control" => "no-cache"
  )
  try
    response = HTTP.request(
      "POST",
      url,
      request_headers;
      retry = false
    )
    return GeminiResponse(response.status, JSON.parse(String(response.body)))
  catch err
    if isa(err, HTTP.ExceptionRequest.StatusError)
      if ==(err.status, 503)
        return GeminiResponse(err.status, JSON.parse(String(err.response.body)))
      else
        return GeminiResponse(err.status, err.response.body)
      end
    end
  end
end

"""
This endpoint retrieves all available symbols for trading

https://docs.gemini.com/rest-api/#symbols

# Arguments:
- `sandbox::Bool`: Sandbox request
"""
function symbols(sandbox::Bool)
  if sandbox
    url = "https://api.sandbox.gemini.com/v1/symbols"
  else
    url = "https://api.gemini.com/v1/symbols"
  end
  try
    response = HTTP.request("GET", url)
    return GeminiResponse(response.status, JSON.parse(String(response.body)))
  catch err
    if isa(err, HTTP.ExceptionRequest.StatusError)
      if ==(err.status, 503)
        return GeminiResponse(err.status, JSON.parse(String(err.response.body)))
      else
        return GeminiResponse(err.status, err.response.body)
      end
    end
  end
end

"""
This endpoint retrieves extra detail on supported symbols, such as minimum order size, tick size, quote increment and more.

https://docs.gemini.com/rest-api/#symbol-details

# Arguments:
- `sandbox::Bool`: Sandbox request
- `symbol::String`: Trading pair symbol
"""
function symbol_details(sandbox::Bool, symbol::String)
  if sandbox
    url = "https://api.sandbox.gemini.com/v1/symbols/details/"
  else
    url = "https://api.gemini.com/v1/symbols/details/"
  end
  try
    response = HTTP.request("GET", url*symbol)
    return GeminiResponse(response.status, JSON.parse(String(response.body)))
  catch err
    if isa(err, HTTP.ExceptionRequest.StatusError)
      if ==(err.status, 503)
        return GeminiResponse(err.status, JSON.parse(String(err.response.body)))
      else
        return GeminiResponse(err.status, err.response.body)
      end
    end
  end
end

"""
Order events is a private API that gives you information about your orders in real time.

https://docs.gemini.com/websocket-api/#order-events

# Arguments:
- `sandbox::Bool`: Sandbox request
- `channel::Channel{Dict}`: channel to pass data
- `names::Vector`: data feed name subscriptions (l2, candles_1m,...)
- `symbols::Vector`: symbol subscriptions (BTCUSD,...)
"""
#=
function order_events(sandbox::Bool, api_key::String, api_secret::String, channel::Channel{Dict})::GeminiResponse
  if sandbox
    base_url = "wss://api.sandbox.gemini.com"
  else
    base_url = "wss://api.gemini.com"
  end
  payload = Dict(
    "request" => "/v1/order/events",
    "nonce" => string(Dates.datetime2epochms(Dates.now()))
  )
  encoded_payload = encode(JSON.json(payload), "UTF-8")
  b64 = base64encode(encoded_payload)
  signature = hexdigest("SHA384", encode(api_secret, "UTF-8"), b64)
  request_headers = Dict(
    "Content-Type" => "text/plain",
    "Content-Length" => "0",
    "X-GEMINI-APIKEY" => api_key,
    "X-GEMINI-PAYLOAD" => b64,
    "X-GEMINI-SIGNATURE" => signature,
    "Cache-Control" => "no-cache"
  )
  WebSockets.open(base_url*"/v1/order/events") do ws
    if isopen(ws)
      if writeguarded(ws, JSON.json(msg))
        while isopen(ws)
          data, success = readguarded(ws)
          if success
            put!(channel, JSON.parse(String(data))::Dict)
          end
        end
      else
        return GeminiResponse(false, Dict("error"=>"failed to send subscription information to Gemini"))
      end
    else
      return GeminiResponse(false, Dict("error"=>"failed to open websocket"))
    end
  end
end
=#

export GeminiResponse
export marketdata_v2
export new_order
export cancel_order
export symbols
export symbol_details

end # module

max_stars_repo_name: rory-linehan/Gemini

max_issues_repo_name: rory-linehan/Gemini

max_forks_repo_name: rory-linehan/Gemini


text:
# ------------ Vectorized trace ------------ #

struct VectorizedTrace{C <: RecordSite} <: Trace
    subrecords::Vector{C}
    VectorizedTrace{C}() where C = new{C}(Vector{C}())
    VectorizedTrace(arr::Vector{C}) where C <: RecordSite = new{C}(arr)
end

has_top(tr::VectorizedTrace{<: CallSite}, addr::Int) = false
function has_top(tr::VectorizedTrace{<: CallSite}, addr::T) where T <: Tuple
    isempty(addr) && return false
    length(addr) == 1 && return has_top(tr, addr[1])
    has_sub(tr, addr[1]) && has_top(get_sub(tr, addr[1]), addr[2 : end])
end
has_top(tr::VectorizedTrace{ChoiceSite}, addr::Int) = addr < length(tr.subrecords)

get_top(tr::VectorizedTrace{<: ChoiceSite}, addr::Int) = return tr.subrecords[addr]
function get_top(tr::VectorizedTrace{<: CallSite}, addr::T) where T <: Tuple
    isempty(addr) && error("VectorizedTrace (get_top): vectorized trace contains call sites, no choice sites at $addr.")
    length(addr) == 1 && return get_top(tr, addr[1])
    return get_top(get_sub(tr, addr[1]), addr[2 : end])
end

has_sub(tr::VectorizedTrace{<: ChoiceSite}, addr) = false
has_sub(tr::VectorizedTrace{<: CallSite}, addr::Int) = addr <= length(tr.subrecords)
function has_sub(tr::VectorizedTrace{<: CallSite}, addr::T) where T <: Tuple
    isempty(addr) && return false
    length(addr) == 1 && return has_sub(tr, addr[1])
    has_sub(tr, addr[1]) && has_sub(get_sub(tr, addr[1]), addr[2 : end])
end

Base.haskey(tr::VectorizedTrace{<: ChoiceSite}, addr::T) where T <: Address = has_top(tr, addr)
Base.haskey(tr::VectorizedTrace{<: CallSite}, addr::T) where T <: Address = has_sub(tr, addr)
function Base.haskey(tr::VectorizedTrace{<: CallSite}, addr::Tuple) where T <: Address
    isempty(addr) && return false
    length(addr) == 1 && return haskey(tr, addr[1])
    has_sub(tr, addr[1]) && haskey(get_sub(tr, addr[1]), addr[2 : end])
end

get_sub(tr::VectorizedTrace{<: CallSite}, addr::Int) = return tr.subrecords[addr]
function get_sub(tr::VectorizedTrace{<: CallSite}, addr::T) where T <: Tuple
    return tr.subrecords[addr]
end

add_call!(tr::VectorizedTrace{<: CallSite}, cs::CallSite) = push!(tr.subrecords, cs)

add_choice!(tr::VectorizedTrace{<: ChoiceSite}, cs::ChoiceSite) = push!(tr.subrecords, cs)

Base.getindex(vt::VectorizedTrace, addr::Int) = vt.subrecords[addr]
function getindex(vt::VectorizedTrace, addrs...)
    has_sub(vt, addrs) && return get_sub(vt, addrs)
    has_top(vt, addrs) && return get_top(vt, addrs)
    error("VectorizedTrace (getindex): no choice or call at $addrs")
end

# ------------ Utility used for pretty printing ------------ #

function collect!(par::T, addrs::Vector{Any}, chd::Dict{Any, Any}, tr::VectorizedTrace, meta) where T <: Tuple
    for (k, v) in enumerate(tr.subrecords)
        if v isa ChoiceSite
            push!(addrs, (par..., k))
            chd[(par..., k)] = v.val
        else
            collect!((par..., k), addrs, chd, v.trace, meta)
        end
    end
end

function collect!(addrs::Vector{Any}, chd::Dict{Any, Any}, tr::VectorizedTrace, meta)
    for (k, v) in enumerate(tr.subrecords)
        if v isa ChoiceSite
            push!(addrs, (k, ))
            chd[k] = v.val
        else
            collect!((k, ), addrs, chd, v.trace, meta)
        end
    end
end

# ------------ Vectorized site ------------ #

struct VectorizedCallSite{F, D, C <: RecordSite, J, K} <: CallSite
    trace::VectorizedTrace{C}
    score::Float64
    fn::D
    len::Int
    args::J
    ret::Vector{K}
    function VectorizedCallSite{F}(sub::VectorizedTrace{C}, sc::Float64, fn::D, len::Int, args::J, ret::Vector{K}) where {F, D, C <: RecordSite, J, K}
        new{F, D, C, J, K}(sub, sc, fn, len, args, ret)
    end
end

has_top(vcs::VectorizedCallSite, addr) = has_top(vcs.trace, addr)

get_top(vcs::VectorizedCallSite, addr) = get_top(vcs.trace, addr)

has_sub(vcs::VectorizedCallSite, addr) = has_sub(vcs.trace, addr)

get_sub(vcs::VectorizedCallSite, addr) = get_sub(vcs.trace, addr)

get_score(vcs::VectorizedCallSite) = vcs.score

getindex(vcs::VectorizedCallSite, addrs...) = getindex(vcs.trace, addrs...)

haskey(vcs::VectorizedCallSite, addrs...) = haskey(vcs.trace, addrs...)

get_ret(cs::VectorizedCallSite) = cs.ret

# ------------ Vectorized discard trace ------------ #

struct VectorizedDiscard <: Trace
    subrecords::Dict{Int, RecordSite}
    VectorizedDiscard() = new(Dict{Int, RecordSite}())
end

get_sub(tr::VectorizedDiscard, addr) = return tr.subrecords[addr]

add_call!(tr::VectorizedDiscard, addr::Int, cs::CallSite) = tr.subrecords[addr] = cs

Base.getindex(vt::VectorizedDiscard, addr::Int) = vt.subrecords[addr]


text:
using DrWatson
@quickactivate "EPExperiments"

using EPExperiments
using Random
using JLD2
using FileIO
using Parameters
using Distributions

@with_kw struct SIRParameters
    tmax::Float32 = 15.0
    total_population::Int = 10_000
    i_0_true::Int = 10
    β_true::Float32 = 0.25
    γ::Float32 = 0.25
    neg_bin_dispersion_param::Float32 = 10
    seed::Int = 1234
end

function generate_data(params::SIRParameters)
    @unpack tmax, total_population, i_0_true, β_true, γ, neg_bin_dispersion_param, seed = params

    Random.seed!(seed)
    tspan = (0.0,tmax)
    obstimes = 1.0:1.0:tmax

    I0 = i_0_true * 10
	S0 = total_population - I0
	u0 = [S0, I0, 0.0, 0.0] # S,I.R,C

    # Fixed parameters.
    p = [β_true, γ]

    prob_ode = ODEProblem(sir_ode!,u0,tspan,p)
    sol_ode = solve(prob_ode, Tsit5(), saveat = 1.0)

    C = Array(sol_ode)[4,:] # Cumulative cases
    X = C[2:end] - C[1:(end-1)]
    #Y = rand.(Poisson.(X))
    Y = rand.([NegativeBinomial(neg_bin_params2(m, 10)...) for m in X])
    @tagsave(datadir("sims", "neg_binomial_data.jld2"), Dict(
        "params" => params,
        "Y" => Y,
        "X" => X
    ))
end

generate_data(SIRParameters())


text:
@safetestset gray_code_symmetry = "Gray code symmetry" begin
using BijectiveHilbert
# for i in 0:10
#     println(lpad(i, 3, " "), " ", lpad(string(BijectiveHilbert.brgc(i), base = 2), 8, "0"))
# end
# Here's a test of the gray code.
n = 5
for i in 0x0:(0x1 << n - 0x1)
    # println(BijectiveHilbert.brgc(1<<n - 1 - i), " ", BijectiveHilbert.brgc(i) ⊻ (1 << (n-1)))
    @test(BijectiveHilbert.brgc(1<<n - 1 - i) == BijectiveHilbert.brgc(i) ⊻ (1 << (n - 1)))
end
end

@safetestset gray_code_single = "Gray code changes one bit" begin
using BijectiveHilbert: brgc, is_power_of_two
for i in 0x1:0x1000
    @test is_power_of_two(brgc(i) ⊻ brgc(i + 1))
end
end

@safetestset brgc_own_inverse = "Gray code is own inverse" begin
using BijectiveHilbert
for i in 0x0:0x1000
    @test(BijectiveHilbert.brgc_inv(BijectiveHilbert.brgc(i)) == i)
end
end

@safetestset brgc_equals_naive = "Gray code matches paper description" begin
using BijectiveHilbert: brgc_inv
using Random
rng = MersenneTwister(9719742)
for T in [UInt8, UInt16, UInt32, UInt64]
    for trial in 1:10000
        v = rand(rng, T)
        n = brgc_inv(Int128(v))  # The paper version is used for integers.
        i = brgc_inv(v)
        @test n == i
    end
end
end

@safetestset brgc_ranks_equal = "the rank calculation is the same as the paper" begin
    using BijectiveHilbert: brgc_rank, brgc_rank2
    using Random
    rng = MersenneTwister(974073242)
    for trial in 1:1000
        n = rand(rng, 2:7)
        w = UInt8(rand(rng, 0:(1<<n - 1)))
        mask = UInt8(rand(rng, 0:(1<<n - 1)))
        a = brgc_rank(mask, w, n)
        b = brgc_rank2(mask, w, n)
        @test a == b
    end
end


text:
"""
    RationalQuadraticKernel(; α=2.0)

The rational-quadratic kernel is a Mercer kernel given by the formula:
```
    κ(x, y) = (1 + ||x − y||² / (2α))^(-α)
```
where `α` is a shape parameter of the Euclidean distance. Check
[`GammaRationalQuadraticKernel`](@ref) for a generalization.
"""
struct RationalQuadraticKernel{Tα<:Real} <: SimpleKernel
    α::Vector{Tα}
    function RationalQuadraticKernel(;alpha::T=2.0, α::T=alpha) where {T}
        @check_args(RationalQuadraticKernel, α, α > zero(T), "α > 0")
        return new{T}([α])
    end
end

@functor RationalQuadraticKernel

function kappa(κ::RationalQuadraticKernel, d²::T) where {T<:Real}
    return (one(T) + d² / (2 * first(κ.α)))^(-first(κ.α))
end

metric(::RationalQuadraticKernel) = SqEuclidean()

function Base.show(io::IO, κ::RationalQuadraticKernel)
    print(io, "Rational Quadratic Kernel (α = $(first(κ.α)))")
end

"""
`GammaRationalQuadraticKernel([α=2.0 [, γ=2.0]])`

The Gamma-rational-quadratic kernel is an isotropic Mercer kernel given by the formula:
```
    κ(x, y) = (1 + ||x−y||^γ / α)^(-α)
```
where `α` is a shape parameter of the Euclidean distance and `γ` is another shape parameter.
"""
struct GammaRationalQuadraticKernel{Tα<:Real, Tγ<:Real} <: SimpleKernel
    α::Vector{Tα}
    γ::Vector{Tγ}
    function GammaRationalQuadraticKernel(
        ;alpha::Tα=2.0, gamma::Tγ=2.0, α::Tα=alpha, γ::Tγ=gamma,
    ) where {Tα<:Real, Tγ<:Real}
        @check_args(GammaRationalQuadraticKernel, α, α > zero(Tα), "α > 0")
        @check_args(GammaRationalQuadraticKernel, γ, zero(γ) < γ <= 2, "0 < γ <= 2")
        return new{Tα, Tγ}([α], [γ])
    end
end

@functor GammaRationalQuadraticKernel

function kappa(κ::GammaRationalQuadraticKernel, d²::Real)
    return (one(d²) + d²^(first(κ.γ) / 2) / first(κ.α))^(-first(κ.α))
end

metric(::GammaRationalQuadraticKernel) = SqEuclidean()

function Base.show(io::IO, κ::GammaRationalQuadraticKernel)
    print(io, "Gamma Rational Quadratic Kernel (α = $(first(κ.α)), γ = $(first(κ.γ)))")
end


text: @test UncertainIndexDataset(rand(30)) isa UncertainIndexDataset


text:
export RadiationModel, NoRadiation

abstract type RadiationModel end

vars_state_conservative(::RadiationModel, FT) = @vars()
vars_state_auxiliary(::RadiationModel, FT) = @vars()
vars_integrals(::RadiationModel, FT) = @vars()
vars_reverse_integrals(::RadiationModel, FT) = @vars()

function atmos_nodal_update_auxiliary_state!(
    ::RadiationModel,
    ::AtmosModel,
    state::Vars,
    aux::Vars,
    t::Real,
) end
function integral_set_auxiliary_state!(
    ::RadiationModel,
    integ::Vars,
    state::Vars,
    aux::Vars,
) end
function integral_load_auxiliary_state!(
    ::RadiationModel,
    aux::Vars,
    integ::Vars,
) end
function reverse_integral_set_auxiliary_state!(
    ::RadiationModel,
    integ::Vars,
    state::Vars,
    aux::Vars,
) end
function reverse_integral_load_auxiliary_state!(
    ::RadiationModel,
    aux::Vars,
    integ::Vars,
) end
function flux_radiation!(
    ::RadiationModel,
    atmos::AtmosModel,
    flux::Grad,
    state::Vars,
    aux::Vars,
    t::Real,
) end

struct NoRadiation <: RadiationModel end


text:
#
# fixme combines
#
using DataFrames
using CSV
using Utils
using Definitions

const TO_Q = 4
const TO_Y = 2019

Uprate_Map = Dict(
    upr_earnings => :average_earnings,
    upr_housing_rents => :actual_rents_for_housing,
    upr_housing_oo => :mortgage_interest_payments,
    upr_unearned => :nominal_gdp,
    upr_costs => :cpi,
    upr_cpi => :cpi,
    upr_gdp_deflator => :gdp_deflator,
    upr_nominal_gdp => :nominal_gdp,
    upr_shares => :equity_prices
)

function make_uprate_types() :: Dict
    d = Dict()
    for i in instances( Incomes_Type )
        if i <= odd_jobs || i in [trade_unions_etc,friendly_societies,work_expenses,avcs,other_deductions]
            d[i] = upr_earnings
        elseif i in [
            private_pensions,
            national_savings,
            bank_interest,
            individual_savings_account,
            # dividends,
            property,
            royalties,
            bonds_and_gilts,
            other_investment_income,
            other_income,
            pension_contributions,
            loan_repayments ]
            d[i] = upr_nominal_gdp
        elseif i in [stocks_shares]
            d[i] = upr_shares
        elseif i in [
            alimony_and_child_support_received,
            health_insurance,
            alimony_and_child_support_paid,
            care_insurance,
            student_loan_repayments,

            education_allowances,
            foster_care_payments,
            student_grants,
            student_loans,
            free_school_meals ]
            d[i] = upr_cpi
        else # keep bens as they are: freeze, plus we just need that a payment has been made
            d[i] = upr_no_uprate
        end
    end

    d[A_Current_account] = upr_nominal_gdp
    d[A_NSB_Ordinary_account] = upr_nominal_gdp
    d[A_NSB_Investment_account] = upr_nominal_gdp
    d[A_Not_Used] = upr_nominal_gdp
    d[A_Savings_investments_etc] = upr_nominal_gdp
    d[A_Government_Gilt_Edged_Stock] = upr_nominal_gdp
    d[A_Unit_or_Investment_Trusts] = upr_shares
    d[A_Stocks_Shares_Bonds_etc] = upr_shares
    d[A_PEP] = upr_nominal_gdp
    d[A_National_Savings_capital_bonds] = upr_nominal_gdp
    d[A_Index_Linked_National_Savings_Certificates] = upr_nominal_gdp
    d[A_Fixed_Interest_National_Savings_Certificates] = upr_nominal_gdp
    d[A_Pensioners_Guaranteed_Bonds] = upr_nominal_gdp
    d[A_SAYE] = upr_nominal_gdp
    d[A_Premium_bonds] = upr_nominal_gdp
    d[A_National_Savings_income_bonds] = upr_nominal_gdp
    d[A_National_Savings_deposit_bonds] = upr_nominal_gdp
    d[A_First_Option_bonds] = upr_nominal_gdp
    d[A_Yearly_Plan] = upr_nominal_gdp
    d[A_ISA] = upr_nominal_gdp
    d[A_Fixd_Rate_Svngs_Bonds_or_Grntd_Incm_Bonds_or_Grntd_Growth_Bonds] = upr_nominal_gdp
    d[A_GEB] = upr_nominal_gdp
    d[A_Basic_Account] = upr_nominal_gdp
    d[A_Credit_Unions] = upr_nominal_gdp
    d[A_Endowment_Policy_Not_Linked] = upr_nominal_gdp

    d
end

UPRATE_MAPPINGS =  make_uprate_types()

"""
Load Quarterly OBR data into a dataframe, and recast everything relative to the target date (Y,Q).
See docs/notes.md on the data. Dataframe is a private global.
"""
function load_prices() :: DataFrame
    obr = CSV.File("$(PRICES_DIR)/merged_quarterly.tab"; delim = '\t', comment = "#") |>
          DataFrame
    nrows = size(obr)[1]
    ncols = size(obr)[2]
    lcnames = Symbol.(basiccensor.(string.(names(obr))))
    rename!(obr, lcnames)

    obr[!,:year] = zeros(Int64, nrows)
    obr[!,:q] = zeros(Int8, nrows) #zeros(Union{Int64,Missing},np)
    dp = r"([0-9]{4})Q([1-4])"
    for i in 1:nrows
        rc = match(dp, obr[i, :date])
        if (rc != nothing)
            obr[i, :year] = parse(Int64, rc[1])
            obr[i, :q] = parse(Int8, rc[2])
        end
    end

    pnew = findfirst((obr.year.==TO_Y) .& (obr.q.==TO_Q))
    for col in 1:ncols
        baser = obr[pnew,col]
        # println( "on col $col $(lcnames[col]); baser=$baser")
        if ! (lcnames[col] in [:q, :year, :date ]) # got to be a better way
            obr[!,col] .= baser./obr[!,col]
        end
    end
    # print(obr[1,:nomi])
    obr
end

OBR_DATA = load_prices()

function uprate( item :: Number, from_y::Integer, from_q::Integer, itype::Uprate_Item_Type)::Number
    # FIXME this is likely much too slow..
    if itype == upr_no_uprate
        return item
    end
    global Uprate_Map
    global OBR_DATA
    global FROM_Y, FROM_Q
    colsym = Uprate_Map[itype]
    p = OBR_DATA[((OBR_DATA.year.==from_y).&(OBR_DATA.q.==from_q)), colsym][1]
    return item * p
end