Rework code, fix/add tests, fix/add docs. All the things.
This commit is contained in:
parent
dfa9f123de
commit
d009acf24b
@ -33,5 +33,6 @@ build_script:
|
|||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- C:\projects\julia\bin\julia -e "Pkg.test(\"PkgTemplates\")"
|
- C:\projects\julia\bin\julia -e "Pkg.test(\"PkgTemplates\")"
|
||||||
|
|
||||||
after_script:
|
after_script:
|
||||||
- C:\projects\julia\bin\julia -e "cd(Pkg.dir(\"AppVeyorTesting\")); Pkg.add(\"Coverage\"); using Coverage; Codecov.submit(process_folder())"
|
- C:\projects\julia\bin\julia -e "cd(Pkg.dir(\"AppVeyorTesting\")); Pkg.add(\"Coverage\"); using Coverage; Codecov.submit(process_folder())"
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,8 +1,6 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
# CodeCov generated files
|
|
||||||
*.jl.cov
|
*.jl.cov
|
||||||
*.jl.*.cov
|
*.jl.*.cov
|
||||||
*.jl.mem
|
*.jl.mem
|
||||||
# Documenter generated files
|
|
||||||
/docs/build/
|
/docs/build/
|
||||||
/docs/site/
|
/docs/site/
|
||||||
|
@ -12,8 +12,6 @@ script:
|
|||||||
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
|
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
|
||||||
- julia -e 'Pkg.clone(pwd()); Pkg.build("PkgTemplates"); Pkg.test("PkgTemplates"; coverage=true)'
|
- julia -e 'Pkg.clone(pwd()); Pkg.build("PkgTemplates"); Pkg.test("PkgTemplates"; coverage=true)'
|
||||||
after_success:
|
after_success:
|
||||||
# push coverage results to CodeCov
|
|
||||||
- julia -e 'cd(Pkg.dir("PkgTemplates")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())'
|
- julia -e 'cd(Pkg.dir("PkgTemplates")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())'
|
||||||
# build documentation
|
|
||||||
- julia -e 'Pkg.add("Documenter")'
|
- julia -e 'Pkg.add("Documenter")'
|
||||||
- julia -e 'cd(Pkg.dir("PkgTemplates")); include(joinpath("docs", "make.jl"))'
|
- julia -e 'cd(Pkg.dir("PkgTemplates")); include(joinpath("docs", "make.jl"))'
|
||||||
|
193
CONTRIBUTING.md
193
CONTRIBUTING.md
@ -1,193 +0,0 @@
|
|||||||
# Contributing to PkgTemplates
|
|
||||||
|
|
||||||
The best way to contribute to `PkgTemplates` is by adding new plugins.
|
|
||||||
|
|
||||||
There are two main types of plugins:
|
|
||||||
[`GenericPlugin`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#GenericPlugin-1)s
|
|
||||||
and
|
|
||||||
[`CustomPlugin`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#CustomPlugin-1)s.
|
|
||||||
|
|
||||||
## Writing a Generic Plugin
|
|
||||||
|
|
||||||
As the name suggests, generic plugins are simpler than custom ones, and as
|
|
||||||
such are extremely easy to implement. They have the ability to add patterns
|
|
||||||
the the generated `.gitignore`, as well as create a single configuration file.
|
|
||||||
We're going to define a new generic plugin `MyPlugin` in
|
|
||||||
`src/plugins/myplugin.jl`:
|
|
||||||
|
|
||||||
```julia
|
|
||||||
@auto_hash_equals struct MyPlugin <: GenericPlugin
|
|
||||||
gitignore::Vector{AbstractString}
|
|
||||||
src::Nullable{AbstractString}
|
|
||||||
dest::AbstractString
|
|
||||||
badges::Vector{AbstractString}
|
|
||||||
view::Dict{String, Any}
|
|
||||||
|
|
||||||
function MyPlugin(; config_file::Union{AbstractString, Void}="")
|
|
||||||
if config_file != nothing
|
|
||||||
if isempty(config_file)
|
|
||||||
config_file = joinpath(DEFAULTS_DIR, "myplugin.yml")
|
|
||||||
elseif !isfile(config_file)
|
|
||||||
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
new([], config_file, ".myplugin.yml", [], Dict{String, Any}())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
That's all there is to it! Let's take a better look at what we've done:
|
|
||||||
|
|
||||||
* The plugin has five attributes, these must be exactly as they are.
|
|
||||||
* `gitignore` is the array of patterns to add the the generated package's
|
|
||||||
`.gitignore`, we chose not to add any with this plugin.
|
|
||||||
* `src` is the location of the config file we're going to copy into the
|
|
||||||
generated package repository. If this is `nothing`, no config file will be
|
|
||||||
generated. This came from the `config_file` keyword argument, which
|
|
||||||
defaulted to an empty string. That's because we've placed a default
|
|
||||||
config file at `defaults/myplugin.yml`.
|
|
||||||
* `dest` is the path to our generated config file, relative to the root of
|
|
||||||
the package repository. In this example, the file will go in
|
|
||||||
`.myplugin.yml` at the root of the repository.
|
|
||||||
* `badges` is an array of Markdown-formatted badge strings to be displayed
|
|
||||||
on the package's README. We chose not to include any here. TODO talk about
|
|
||||||
`substitute`.
|
|
||||||
* `view` is a dictionary of additional replacements to `substitute`.
|
|
||||||
|
|
||||||
Plenty of services like
|
|
||||||
[`TravisCI`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#TravisCI-1)
|
|
||||||
and
|
|
||||||
[`CodeCov`](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugins.html#CodeCov-1)
|
|
||||||
follow this format, so generic plugins should be able to get you pretty far.
|
|
||||||
|
|
||||||
## Writing a Custom Plugin
|
|
||||||
|
|
||||||
When a service doesn't follow the pattern demonstrated above, it's time to write a custom
|
|
||||||
plugin. These are still pretty simple, needing at most two additional methods. Let's create
|
|
||||||
a custom plugin called `Gamble` in `src/plugins/gamble.jl` that only generates a file if
|
|
||||||
you get lucky enough:
|
|
||||||
|
|
||||||
```julia
|
|
||||||
@auto_hash_equals struct Gamble <: CustomPlugin
|
|
||||||
gitignore:Vector{AbstractString}
|
|
||||||
src::AbstractString
|
|
||||||
success::Bool
|
|
||||||
|
|
||||||
function Gamble(config_file::AbstractString)
|
|
||||||
if !isfile(config_file)
|
|
||||||
throw(ArgumentError("File $(abspath(config_file)) does not exist"))
|
|
||||||
end
|
|
||||||
success = rand() > 0.8
|
|
||||||
println(success ? "Congratulations!" : "Maybe next time.")
|
|
||||||
new([], config_file, success)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function badges(plugin: Gamble, user::AbstractString, pkg_name::AbstractString)
|
|
||||||
if plugin.success
|
|
||||||
return ["[](https://pokerstars.net)"]
|
|
||||||
else
|
|
||||||
return String[]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function gen_plugin(plugin::Gamble, template::Template, pkg_name::AbstractString)
|
|
||||||
if plugin.success
|
|
||||||
text = substitute(readstring(plugin.src), template, pkg_name)
|
|
||||||
gen_file(joinpath(t.temp_dir, ".gambler.yml"), text)
|
|
||||||
return [".gambler.yml"]
|
|
||||||
else
|
|
||||||
return String[]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
With that, we've got everything we need. Note that this plugin still has a `gitignore`
|
|
||||||
attribute; it's required for all plugins. Let's look at the extra methods we implemented:
|
|
||||||
|
|
||||||
#### `gen_plugin`
|
|
||||||
|
|
||||||
We read the text from the plugin's source file, and then we run it through the `substitute`
|
|
||||||
function (more on that [later](#template-substitution)).
|
|
||||||
|
|
||||||
Next, we use `gen_file` to write the text, with substitutions applied, to the destination
|
|
||||||
file in `t.temp_dir`. Generating our repository in a temp directory means we're not stuck
|
|
||||||
with leftovers in the case of an error.
|
|
||||||
|
|
||||||
This function returns an array of all the root-level files or directories
|
|
||||||
that were created. If both `foo/bar` and `foo/baz` were created, we only need
|
|
||||||
to return `["foo/"]`.
|
|
||||||
|
|
||||||
#### `badges`
|
|
||||||
|
|
||||||
This function returns an array of Markdown-formatted badges to be displayed on
|
|
||||||
the package README. You can find badges and Markdown strings for just about
|
|
||||||
everything on [Shields.io](https://shields.io).
|
|
||||||
|
|
||||||
This will do the trick, but if we want our badge to appear at a specific
|
|
||||||
position in the README, we need to edit `BADGE_ORDER` in
|
|
||||||
[`src/PkgTemplates.jl`(https://github.com/invenia/PkgTemplates.jl/blob/master/src/PkgTemplates.jl).
|
|
||||||
Say we want our badge to appear before all others, we'll add `Gamble` to the
|
|
||||||
beginning of the array.
|
|
||||||
|
|
||||||
That's all there is to it! We've just created a nifty custom plugin.
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### Template Substitution
|
|
||||||
|
|
||||||
Since plugin configuration files are often specific to the package they belong
|
|
||||||
to, we might want to replace some placeholder values in our plugin's config
|
|
||||||
file. We can do this by following
|
|
||||||
[Mustache.jl](https://github.com/jverzani/Mustache.jl)'s rules. Some
|
|
||||||
replacements are defined by `PkgTemplates`:
|
|
||||||
|
|
||||||
* `{{PKGNAME}}` is replaced by `pkg_name`.
|
|
||||||
* `{{VERSION}}` is replaced by `$major.$minor` corresponding to
|
|
||||||
`template.julia_version`.
|
|
||||||
|
|
||||||
Some conditional replacements are also defined:
|
|
||||||
|
|
||||||
* `{{DOCUMENTER}}Documenter{{/DOCUMENTER}}`
|
|
||||||
* "Documenter" only appears in the rendered text if the template contains
|
|
||||||
a [`Documenter`](src/plugins/documenter.jl) subtype.
|
|
||||||
* `{{CODECOV}}CodeCov{{/CODECOV}}`
|
|
||||||
* "CodeCov" only appears in the rendered text if the template contains
|
|
||||||
the [`CodeCov`](src/plugins/codecov.jl) plugin.
|
|
||||||
* `{{#AFTER}}After{{/AFTER}}`
|
|
||||||
* "After" only appears in the rendered text if something needs to happen
|
|
||||||
**after** CI testing occurs. As of right now, this is true when either of
|
|
||||||
the above two conditions are true.
|
|
||||||
|
|
||||||
We can also specify our own replacements by passing a dictionary to
|
|
||||||
`substitute`:
|
|
||||||
|
|
||||||
```julia
|
|
||||||
view = Dict("KEY" => "VAL", "HEADS" => 2rand() > 1)
|
|
||||||
text = """
|
|
||||||
{{KEY}}
|
|
||||||
{{PKGNAME}}
|
|
||||||
{{#HEADS}}Heads{{/HEADS}}
|
|
||||||
"""
|
|
||||||
substituted = substitute(text, "MyPkg", template; view=view)
|
|
||||||
```
|
|
||||||
|
|
||||||
This will return `"VAL\nMyPkg\nHeads\n"` if `2rand() > 1` was true,
|
|
||||||
`"VAL\nMyPkg\n\n"` otherwise.
|
|
||||||
|
|
||||||
Note the double newline in the second outcome; `Mustache` has a bug with
|
|
||||||
conditionals that inserts extra newlines (more detail
|
|
||||||
[here](https://github.com/jverzani/Mustache.jl/issues/47)). We can get around
|
|
||||||
this by writing ugly template files, like so:
|
|
||||||
|
|
||||||
```
|
|
||||||
{{KEY}}
|
|
||||||
{{PKGNAME}}{{#HEADS}}
|
|
||||||
Heads{{/HEADS}}
|
|
||||||
```
|
|
||||||
|
|
||||||
The resulting string will end with a single newline regardless of the value
|
|
||||||
of `view["HEADS"]`
|
|
||||||
|
|
||||||
Also note that conditionals without a corresponding key in `view` won't error,
|
|
||||||
but will simply be evaluated as false.
|
|
@ -85,5 +85,11 @@ Information on each keyword as well as plugin types can be found in the
|
|||||||
`PkgTemplates` is similar in functionality to `PkgDev`'s `generate` function.
|
`PkgTemplates` is similar in functionality to `PkgDev`'s `generate` function.
|
||||||
However, `PkgTemplates` offers more customizability in templates and more
|
However, `PkgTemplates` offers more customizability in templates and more
|
||||||
extensibility via plugins. For the package registration and release management
|
extensibility via plugins. For the package registration and release management
|
||||||
features that `PkgTemplates` lacks, you are encouraged to use
|
features that `PkgTemplates` doesn't include, you are encouraged to use
|
||||||
[AttoBot](https://github.com/apps/attobot) instead.
|
[AttoBot](https://github.com/apps/attobot) instead.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
It's extremely easy to extend `PkgTemplates` with new plugins. To get started,
|
||||||
|
check out the
|
||||||
|
[plugin development guide](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugin_development.html).
|
||||||
|
@ -32,6 +32,8 @@ build_script:
|
|||||||
Pkg.clone(pwd(), \"{{PKGNAME}}\"); Pkg.build(\"{{PKGNAME}}\")"
|
Pkg.clone(pwd(), \"{{PKGNAME}}\"); Pkg.build(\"{{PKGNAME}}\")"
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- C:\projects\julia\bin\julia -e "Pkg.test(\"{{PKGNAME}}\")"
|
- C:\projects\julia\bin\julia -e "Pkg.test(\"{{PKGNAME}}\")"{{#AFTER}}
|
||||||
{{#AFTER}}after_script:
|
|
||||||
{{#CODECOV}}- C:\projects\julia\bin\julia -e "cd(Pkg.dir(\"{{PKGNAME}}\")); Pkg.add(\"Coverage\"); using Coverage; Codecov.submit(process_folder())"{{/CODECOV}}{{/AFTER}}
|
after_script:{{#CODECOV}}
|
||||||
|
- C:\projects\julia\bin\julia -e "cd(Pkg.dir(\"{{PKGNAME}}\")); Pkg.add(\"Coverage\"); using Coverage; Codecov.submit(process_folder())"{{/CODECOV}}{{#COVERALLS}}
|
||||||
|
- C:\projects\julia\bin\julia -e "cd(Pkg.dir(\"{{PKGNAME}}\")); Pkg.add(\"Coverage\"); using Coverage; Coveralls.submit(process_folder())"{{/COVERALLS}}{{/AFTER}}
|
||||||
|
@ -10,8 +10,9 @@ notifications:
|
|||||||
email: false
|
email: false
|
||||||
script:
|
script:
|
||||||
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
|
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
|
||||||
- julia -e 'Pkg.clone(pwd()); Pkg.build("{{PKGNAME}}"); Pkg.test("{{PKGNAME}}"; coverage=true)'
|
- julia -e 'Pkg.clone(pwd()); Pkg.build("{{PKGNAME}}"); Pkg.test("{{PKGNAME}}"; coverage=true)'{{#AFTER}}
|
||||||
{{#AFTER}}after_success:
|
after_success:{{#CODECOV}}
|
||||||
{{#CODECOV}}- julia -e 'cd(Pkg.dir("{{PKGNAME}}")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())'{{/CODECOV}}
|
- julia -e 'cd(Pkg.dir("{{PKGNAME}}")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())'{{/CODECOV}}{{#COVERALLS}}
|
||||||
{{#DOCUMENTER}}- julia -e 'Pkg.add("Documenter")'
|
- julia -e 'cd(Pkg.dir("{{PKGNAME}}")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(process_folder())'{{/COVERALLS}}{{#DOCUMENTER}}
|
||||||
|
- julia -e 'Pkg.add("Documenter")'
|
||||||
- julia -e 'cd(Pkg.dir("{{PKGNAME}}")); include(joinpath("docs", "make.jl"))'{{/DOCUMENTER}}{{/AFTER}}
|
- julia -e 'cd(Pkg.dir("{{PKGNAME}}")); include(joinpath("docs", "make.jl"))'{{/DOCUMENTER}}{{/AFTER}}
|
||||||
|
@ -7,6 +7,7 @@ makedocs(
|
|||||||
"Home" => "index.md",
|
"Home" => "index.md",
|
||||||
"Package Generation" => "pages/package_generation.md",
|
"Package Generation" => "pages/package_generation.md",
|
||||||
"Plugins" => "pages/plugins.md",
|
"Plugins" => "pages/plugins.md",
|
||||||
|
"Plugin Development" => "pages/plugin_development.md",
|
||||||
"Licenses" => "pages/licenses.md",
|
"Licenses" => "pages/licenses.md",
|
||||||
"Index" => "pages/index.md"
|
"Index" => "pages/index.md"
|
||||||
],
|
],
|
||||||
|
62
docs/src/pages/plugin_development.md
Normal file
62
docs/src/pages/plugin_development.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
```@meta
|
||||||
|
CurrentModule = PkgTemplates
|
||||||
|
```
|
||||||
|
|
||||||
|
# Plugin Development
|
||||||
|
|
||||||
|
The best and easiest way to contribute to `PkgTemplates` is to write new
|
||||||
|
plugins.
|
||||||
|
|
||||||
|
There are two types of plugins: [`GenericPlugin`](@ref)s and [`CustomPlugin`](@ref)s.
|
||||||
|
|
||||||
|
## Generic Plugins
|
||||||
|
|
||||||
|
```@docs
|
||||||
|
GenericPlugin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Plugins
|
||||||
|
|
||||||
|
```@docs
|
||||||
|
CustomPlugin
|
||||||
|
```
|
||||||
|
|
||||||
|
### CustomPlugin required methods
|
||||||
|
|
||||||
|
#### `gen_plugin`
|
||||||
|
|
||||||
|
```@docs
|
||||||
|
gen_plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `badges`
|
||||||
|
|
||||||
|
```@docs
|
||||||
|
badges
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Functions
|
||||||
|
|
||||||
|
#### gen_file
|
||||||
|
|
||||||
|
```@docs
|
||||||
|
gen_file
|
||||||
|
```
|
||||||
|
|
||||||
|
#### substitute
|
||||||
|
|
||||||
|
```@docs
|
||||||
|
substitute
|
||||||
|
```
|
||||||
|
|
||||||
|
#### badge
|
||||||
|
|
||||||
|
```@docs
|
||||||
|
badge
|
||||||
|
```
|
||||||
|
|
||||||
|
#### version_floor
|
||||||
|
|
||||||
|
```@docs
|
||||||
|
version_floor
|
||||||
|
```
|
@ -4,8 +4,9 @@ CurrentModule = PkgTemplates
|
|||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
|
|
||||||
For information on writing your own plugins, see
|
Plugins are the driver for `PkgTemplates`'s customization and extension. This page
|
||||||
[CONTRIBUTING.md](https://github.com/invenia/PkgTemplates.jl/tree/master/CONTRIBUTING.md).
|
describes plugins that already exist; for information on writing your own plugins, see the
|
||||||
|
[plugin development guide](https://invenia.github.io/PkgTemplates.jl/stable/pages/plugin_development.html).
|
||||||
|
|
||||||
## TravisCI
|
## TravisCI
|
||||||
|
|
||||||
@ -25,6 +26,12 @@ AppVeyor
|
|||||||
CodeCov
|
CodeCov
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Coveralls
|
||||||
|
|
||||||
|
```@docs
|
||||||
|
Coveralls
|
||||||
|
```
|
||||||
|
|
||||||
## Documenter
|
## Documenter
|
||||||
|
|
||||||
```@docs
|
```@docs
|
||||||
|
113
src/generate.jl
113
src/generate.jl
@ -42,8 +42,11 @@ function generate(
|
|||||||
end
|
end
|
||||||
LibGit2.commit(repo, "Empty initial commit")
|
LibGit2.commit(repo, "Empty initial commit")
|
||||||
info("Made initial empty commit")
|
info("Made initial empty commit")
|
||||||
rmt = ssh ? "git@$(t.host):$(t.user)/$pkg_name.jl.git" :
|
rmt = if ssh
|
||||||
|
"git@$(t.host):$(t.user)/$pkg_name.jl.git"
|
||||||
|
else
|
||||||
"https://$(t.host)/$(t.user)/$pkg_name.jl"
|
"https://$(t.host)/$(t.user)/$pkg_name.jl"
|
||||||
|
end
|
||||||
LibGit2.set_remote_url(repo, rmt)
|
LibGit2.set_remote_url(repo, rmt)
|
||||||
info("Set remote origin to $rmt")
|
info("Set remote origin to $rmt")
|
||||||
|
|
||||||
@ -72,7 +75,7 @@ function generate(
|
|||||||
info("Committed files generated by PkgTemplates")
|
info("Committed files generated by PkgTemplates")
|
||||||
multiple_branches = length(collect(LibGit2.GitBranchIter(repo))) > 1
|
multiple_branches = length(collect(LibGit2.GitBranchIter(repo))) > 1
|
||||||
info("Copying temporary package directory into $(t.dir)/")
|
info("Copying temporary package directory into $(t.dir)/")
|
||||||
cp(temp_pkg_dir, pkg_dir; remove_destination=force)
|
mv(temp_pkg_dir, pkg_dir; remove_destination=force)
|
||||||
rm(t.temp_dir; recursive=true)
|
rm(t.temp_dir; recursive=true)
|
||||||
info("Finished")
|
info("Finished")
|
||||||
if multiple_branches
|
if multiple_branches
|
||||||
@ -103,7 +106,7 @@ function gen_readme(pkg_name::AbstractString, template::Template)
|
|||||||
badges(template.plugins[plugin_type], template.user, pkg_name),
|
badges(template.plugins[plugin_type], template.user, pkg_name),
|
||||||
"\n",
|
"\n",
|
||||||
)
|
)
|
||||||
deleteat!(remaining, findin(remaining, plugin_type))
|
deleteat!(remaining, find(p -> p == plugin_type, remaining)[1])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for plugin_type in remaining
|
for plugin_type in remaining
|
||||||
@ -273,71 +276,69 @@ function version_floor(v::VersionNumber=VERSION)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
substitute(template::AbstractString, view::Dict{String, Any}) -> String
|
||||||
|
|
||||||
|
Replace placeholders in a template string. The input string is not modified.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
* `template::AbstracString`: Template string in which to make replacements.
|
||||||
|
* `view::Dict{String, Any}`: (Placeholder => value) pairs.
|
||||||
|
|
||||||
|
Returns the template string with replacements applied.
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
Due to a bug in `Mustache`, conditionals often insert undesired newlines (more detail
|
||||||
|
[here](https://github.com/jverzani/Mustache.jl/issues/47)).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
A
|
||||||
|
{{#B}}B{{/B}}
|
||||||
|
C
|
||||||
|
```
|
||||||
|
|
||||||
|
When `view` doesn't have a `"B"` key (or it does, but it's false), this becomes
|
||||||
|
`"A\\n\\nC"` We can get around this by writing ugly template files, like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
A{{#B}}
|
||||||
|
B{{/B}}
|
||||||
|
C
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, the result is `"A\\nB\\nC"`, like we want it to be.
|
||||||
|
|
||||||
|
Also note that conditionals without a corresponding key in `view` won't error,
|
||||||
|
but will simply be evaluated as false.
|
||||||
|
"""
|
||||||
|
substitute(template::AbstractString, view::Dict{String, Any}) = render(template, view)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
substitute(
|
substitute(
|
||||||
template::AbstractString,
|
template::AbstractString,
|
||||||
pkg_template::Template;
|
pkg_template::Template;
|
||||||
pkg_name::AbstractString,
|
|
||||||
view::Dict{String, Any}=Dict{String, Any}(),
|
view::Dict{String, Any}=Dict{String, Any}(),
|
||||||
) -> String
|
) -> String
|
||||||
|
|
||||||
Replace placeholders in `template`. The input string is not modified.
|
Replace placeholders in template string, using some default replacements based on the
|
||||||
|
package template. The input string is not modified.
|
||||||
# Arguments:
|
|
||||||
* `template::AbstractString`: Template string to make replacements in.
|
|
||||||
* `pkg_template::Template`: The package template in use.
|
|
||||||
* `pkg_name::AbstractString`: Name of the package being created.
|
|
||||||
* `view::Dict{String, Any}=Dict{String, Any}()`: Additional values to be substituted.
|
|
||||||
|
|
||||||
Returns the text with substitutions applied.
|
|
||||||
"""
|
"""
|
||||||
function substitute(
|
function substitute(
|
||||||
template::AbstractString,
|
template::AbstractString,
|
||||||
pkg_template::Template,
|
pkg_template::Template;
|
||||||
pkg_name::AbstractString;
|
|
||||||
view::Dict{String, Any}=Dict{String, Any}(),
|
view::Dict{String, Any}=Dict{String, Any}(),
|
||||||
)
|
)
|
||||||
d = merge!(Dict{String, Any}(), view)
|
|
||||||
d["PKGNAME"] = pkg_name
|
|
||||||
d["USER"] = pkg_template.user
|
|
||||||
v = pkg_template.julia_version
|
|
||||||
# Don't use version_floor here because we don't want the trailing '-' on prereleases.
|
# Don't use version_floor here because we don't want the trailing '-' on prereleases.
|
||||||
d["VERSION"] = "$(v.major).$(v.minor)"
|
v = pkg_template.julia_version
|
||||||
|
d = Dict{String, Any}(
|
||||||
|
"USER" => pkg_template.user,
|
||||||
|
"VERSION" => "$(v.major).$(v.minor)",
|
||||||
|
"DOCUMENTER" => any(isa(p, Documenter) for p in values(pkg_template.plugins)),
|
||||||
|
"CODECOV" => haskey(pkg_template.plugins, CodeCov),
|
||||||
|
"COVERALLS" => haskey(pkg_template.plugins, Coveralls),
|
||||||
|
)
|
||||||
# d["AFTER"] is true whenever something needs to occur in a CI "after_script".
|
# d["AFTER"] is true whenever something needs to occur in a CI "after_script".
|
||||||
if any(isa(p, Documenter) for p in values(pkg_template.plugins))
|
d["AFTER"] = d["DOCUMENTER"] || d["CODECOV"] || d["COVERALLS"]
|
||||||
d["DOCUMENTER"] = true
|
return substitute(template, merge(d, view))
|
||||||
d["AFTER"] = true
|
|
||||||
end
|
|
||||||
if haskey(pkg_template.plugins, CodeCov)
|
|
||||||
d["CODECOV"] = true
|
|
||||||
d["AFTER"] = true
|
|
||||||
end
|
|
||||||
return render(template, d)
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
substitute(
|
|
||||||
template::AbstractString,
|
|
||||||
pkg_plugin::Plugin;
|
|
||||||
pkg_name::AbstractString,
|
|
||||||
view::Dict{String, Any}=Dict{String, Any}(),
|
|
||||||
) -> String
|
|
||||||
|
|
||||||
Replace placeholders in `template`. The input string is not modified.
|
|
||||||
|
|
||||||
# Arguments:
|
|
||||||
* `template::AbstractString`: Template string to make replacements in.
|
|
||||||
* `pkg_plugin::Plugin`: The plugin in use.
|
|
||||||
* `pkg_name::AbstractString`: Name of the package being created.
|
|
||||||
* `view::Dict{String, Any}=Dict{String, Any}()`: Additional values to be substituted.
|
|
||||||
|
|
||||||
Returns the text with substitutions applied.
|
|
||||||
"""
|
|
||||||
function substitute(
|
|
||||||
template::AbstractString,
|
|
||||||
pkg_plugin::Plugin,
|
|
||||||
pkg_name::AbstractString;
|
|
||||||
view::Dict{String, Any}=Dict{String, Any}(),
|
|
||||||
)
|
|
||||||
return render(template, merge(Dict("PKGNAME" => pkg_name), view))
|
|
||||||
end
|
end
|
||||||
|
187
src/plugin.jl
187
src/plugin.jl
@ -1,28 +1,123 @@
|
|||||||
abstract type GenericPlugin <: Plugin end
|
|
||||||
abstract type CustomPlugin <: Plugin end
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
badges(\_::Plugin, user::AbstractString, pkg_name::AbstractString)
|
Generic plugins are plugins that add any number of patterns to the generated package's
|
||||||
|
`.gitignore`, and have at most one associated file to generate.
|
||||||
|
|
||||||
Generate Markdown badges for the current package.
|
# Attributes
|
||||||
|
* `gitignore::Vector{AbstractString}`: Array of patterns to be added to the `.gitignore` of
|
||||||
|
generated packages that use this plugin.
|
||||||
|
* `src::Nullable{AbstractString}`: Path to the file that will be copied into the generated
|
||||||
|
package repository. If set to `nothing`, no file will be generated. When this defaults
|
||||||
|
to an empty string, there should be a default file in `defaults` that will be copied.
|
||||||
|
* `dest::AbstractString`: Path to the generated file, relative to the root of the generated
|
||||||
|
package repository.
|
||||||
|
* `badges::Vector{Vector{AbstractString}}`: Array of arrays containing information to
|
||||||
|
create a Markdown-formatted badge from the plugin. Each entry is of the form
|
||||||
|
`[hover_text, image_url, link_url]`. Entries will be run through [`substitute`](@ref),
|
||||||
|
so they may contain placeholder values.
|
||||||
|
* `view::Dict{String, Any}`: Additional substitutions to make in both the plugin's badges
|
||||||
|
and its associated file. See [`substitute`](@ref) for details.
|
||||||
|
|
||||||
# Arguments
|
# Example
|
||||||
* `plugin::Plugin`: Plugin whose badges we are generating.
|
```julia
|
||||||
* `user::AbstractString`: Username of the package creator.
|
@auto_hash_equals struct MyPlugin <: GenericPlugin
|
||||||
* `pkg_name::AbstractString`: Name of the package.
|
gitignore::Vector{AbstractString}
|
||||||
|
src::Nullable{AbstractString}
|
||||||
|
dest::AbstractString
|
||||||
|
badges::Vector{Vector{AbstractString}}
|
||||||
|
view::Dict{String, Any}
|
||||||
|
|
||||||
Returns an array of Markdown badges.
|
function MyPlugin(; config_file::Union{AbstractString, Void}="")
|
||||||
"""
|
if config_file != nothing
|
||||||
badges(plugin::Plugin, user::AbstractString, pkg_name::AbstractString) = String[]
|
if isempty(config_file)
|
||||||
|
config_file = joinpath(DEFAULTS_DIR, "myplugin.yml")
|
||||||
function badges(plugin::GenericPlugin, user::AbstractString, pkg_name::AbstractString)
|
elseif !isfile(config_file)
|
||||||
return substitute.(
|
throw(ArgumentError(
|
||||||
plugin.badges,
|
"File \$(abspath(config_file)) does not exist"
|
||||||
plugin,
|
))
|
||||||
pkg_name;
|
end
|
||||||
view=Dict{String, Any}("USER" => user)
|
end
|
||||||
)
|
new(
|
||||||
|
["*.mgp"],
|
||||||
|
config_file,
|
||||||
|
".mypugin.yml",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"My Plugin",
|
||||||
|
"https://myplugin.com/badge-{{YEAR}}.png",
|
||||||
|
"https://myplugin.com/{{USER}}/{{PKGNAME}}.jl",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
Dict{String, Any}("YEAR" => Dates.year(Dates.now())),
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The above plugin ignores files ending with `.mgp`, copies `defaults/myplugin.yml` by
|
||||||
|
default, and creates a badge that links to the project on its own site, using the default
|
||||||
|
substitutions with one addition: `{{YEAR}} => Dates.year(Dates.now())`.
|
||||||
|
"""
|
||||||
|
abstract type GenericPlugin <: Plugin end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Custom plugins are plugins whose behaviour does not follow the [`GenericPlugin`](@ref)
|
||||||
|
pattern. They can implement [`gen_plugin`](@ref) and [`badges`](@ref) in any way they
|
||||||
|
choose.
|
||||||
|
|
||||||
|
# Attributes
|
||||||
|
* `gitignore::Vector{AbstractString}`: Array of patterns to be added to the `.gitignore` of
|
||||||
|
generated packages that use this plugin.
|
||||||
|
|
||||||
|
# Example
|
||||||
|
|
||||||
|
```julia
|
||||||
|
@auto_hash_equals struct MyPlugin <: CustomPlugin
|
||||||
|
gitignore::Vector{AbstractString}
|
||||||
|
lucky::Bool
|
||||||
|
|
||||||
|
MyPlugin() = new([], rand() > 0.8)
|
||||||
|
|
||||||
|
function gen_plugin(
|
||||||
|
plugin::MyPlugin,
|
||||||
|
template::Template,
|
||||||
|
pkg_name::AbstractString
|
||||||
|
)
|
||||||
|
if plugin.lucky
|
||||||
|
text = substitute(
|
||||||
|
"You got lucky with {{PKGNAME}}, {{USER}}!"),
|
||||||
|
template,
|
||||||
|
)
|
||||||
|
gen_file(joinpath(template.temp_dir, ".myplugin.yml"), text)
|
||||||
|
else
|
||||||
|
println("Maybe next time.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function badges(
|
||||||
|
plugin::MyPlugin,
|
||||||
|
user::AbstractString,
|
||||||
|
pkg_name::AbstractString,
|
||||||
|
)
|
||||||
|
if plugin.lucky
|
||||||
|
return [
|
||||||
|
badge(
|
||||||
|
"You got lucky!",
|
||||||
|
"https://myplugin.com/badge.png",
|
||||||
|
"https://myplugin.com/\$user/\$pkg_name.jl",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
else
|
||||||
|
return String[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This plugin doesn't do much, but it demonstrates how [`gen_plugin`](@ref) and
|
||||||
|
[`badges`](@ref) can be implemented using [`substitute`](@ref), [`gen_file`](@ref),
|
||||||
|
and [`badge`](@ref).
|
||||||
|
"""
|
||||||
|
abstract type CustomPlugin <: Plugin end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
gen_plugin(plugin::Plugin, template::Template, pkg_name::AbstractString) -> Vector{String}
|
gen_plugin(plugin::Plugin, template::Template, pkg_name::AbstractString) -> Vector{String}
|
||||||
@ -39,16 +134,50 @@ Returns an array of generated file/directory names.
|
|||||||
gen_plugin(plugin::Plugin, template::Template, pkg_name::AbstractString) = String[]
|
gen_plugin(plugin::Plugin, template::Template, pkg_name::AbstractString) = String[]
|
||||||
|
|
||||||
function gen_plugin(plugin::GenericPlugin, template::Template, pkg_name::AbstractString)
|
function gen_plugin(plugin::GenericPlugin, template::Template, pkg_name::AbstractString)
|
||||||
try
|
src = try
|
||||||
text = substitute(
|
get(plugin.src)
|
||||||
readstring(get(plugin.src)),
|
|
||||||
template,
|
|
||||||
pkg_name;
|
|
||||||
view=plugin.view,
|
|
||||||
)
|
|
||||||
gen_file(joinpath(template.temp_dir, pkg_name, plugin.dest), text)
|
|
||||||
return [plugin.dest]
|
|
||||||
catch
|
catch
|
||||||
return String[]
|
return String[]
|
||||||
end
|
end
|
||||||
|
text = substitute(
|
||||||
|
readstring(src),
|
||||||
|
template;
|
||||||
|
view=merge(Dict("PKGNAME" => pkg_name), plugin.view),
|
||||||
|
)
|
||||||
|
gen_file(joinpath(template.temp_dir, pkg_name, plugin.dest), text)
|
||||||
|
return [plugin.dest]
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
badges(plugin::Plugin, user::AbstractString, pkg_name::AbstractString) -> Vector{String}
|
||||||
|
|
||||||
|
Generate Markdown badges for the plugin.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
* `plugin::Plugin`: Plugin whose badges we are generating.
|
||||||
|
* `user::AbstractString`: Username of the package creator.
|
||||||
|
* `pkg_name::AbstractString`: Name of the package.
|
||||||
|
|
||||||
|
Returns an array of Markdown badges.
|
||||||
|
"""
|
||||||
|
badges(plugin::Plugin, user::AbstractString, pkg_name::AbstractString) = String[]
|
||||||
|
|
||||||
|
function badges(plugin::GenericPlugin, user::AbstractString, pkg_name::AbstractString)
|
||||||
|
# Give higher priority to replacements defined in the plugin's view.
|
||||||
|
view = merge(Dict("USER" => user, "PKGNAME" => pkg_name), plugin.view)
|
||||||
|
return [badge([substitute(part, view) for part in b]...) for b in plugin.badges]
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
badge(hover::AbstractString, image::AbstractString, image::AbstractString) -> String
|
||||||
|
|
||||||
|
Format a single Markdown badge.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
* `hover::AbstractString`: Text to appear when the mouse is hovered over the badge.
|
||||||
|
* `image::AbstractString`: URL to the image to display.
|
||||||
|
* `link::AbstractString`: URL to go to upon clicking the badge.
|
||||||
|
"""
|
||||||
|
function badge(hover::AbstractString, image::AbstractString, link::AbstractString)
|
||||||
|
return "[]($link)"
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
AppVeyor(; config_file::Union{AbstractString, Void}="") -> GenericPlugin
|
AppVeyor(; config_file::Union{AbstractString, Void}="") -> GenericPlugin
|
||||||
|
|
||||||
Add AppVeyor to a template's plugins to add AppVeyor CI support.
|
Add `AppVeyor` to a template's plugins to add a `.appveyor.yml` configuration file to
|
||||||
|
generated repositories, and an appropriate badge to the README.
|
||||||
|
|
||||||
# Keyword Arguments
|
# Keyword Arguments
|
||||||
* `config_file::Union{AbstractString, Void}=""`: Path to a custom `.appveyor.yml`.
|
* `config_file::Union{AbstractString, Void}=""`: Path to a custom `.appveyor.yml`.
|
||||||
@ -11,7 +12,7 @@ Add AppVeyor to a template's plugins to add AppVeyor CI support.
|
|||||||
gitignore::Vector{AbstractString}
|
gitignore::Vector{AbstractString}
|
||||||
src::Nullable{AbstractString}
|
src::Nullable{AbstractString}
|
||||||
dest::AbstractString
|
dest::AbstractString
|
||||||
badges::Vector{AbstractString}
|
badges::Vector{Vector{AbstractString}}
|
||||||
view::Dict{String, Any}
|
view::Dict{String, Any}
|
||||||
|
|
||||||
function AppVeyor(; config_file::Union{AbstractString, Void}="")
|
function AppVeyor(; config_file::Union{AbstractString, Void}="")
|
||||||
@ -26,7 +27,13 @@ Add AppVeyor to a template's plugins to add AppVeyor CI support.
|
|||||||
[],
|
[],
|
||||||
config_file,
|
config_file,
|
||||||
".appveyor.yml",
|
".appveyor.yml",
|
||||||
["[](https://ci.appveyor.com/project/{{USER}}/{{PKGNAME}}-jl)"],
|
[
|
||||||
|
[
|
||||||
|
"Build Status",
|
||||||
|
"https://ci.appveyor.com/api/projects/status/github/{{USER}}/{{PKGNAME}}.jl?svg=true",
|
||||||
|
"https://ci.appveyor.com/project/{{USER}}/{{PKGNAME}}-jl",
|
||||||
|
],
|
||||||
|
],
|
||||||
Dict{String, Any}(),
|
Dict{String, Any}(),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
CodeCov(; config_file::Union{AbstractString, Void}="") -> GenericPlugin
|
CodeCov(; config_file::Union{AbstractString, Void}="") -> GenericPlugin
|
||||||
|
|
||||||
Add CodeCov to a template's plugins to enable CodeCov coverage reports.
|
Add `CodeCov` to a template's plugins to add a `.codecov.yml` configuration file to
|
||||||
|
generated repositories, and an appropriate badge to the README. Also updates the
|
||||||
|
`.gitignore` accordingly.
|
||||||
|
|
||||||
# Keyword Arguments:
|
# Keyword Arguments:
|
||||||
* `config_file::Union{AbstractString, Void}=""`: Path to a custom `.codecov.yml`.
|
* `config_file::Union{AbstractString, Void}=""`: Path to a custom `.codecov.yml`.
|
||||||
@ -11,7 +13,7 @@ Add CodeCov to a template's plugins to enable CodeCov coverage reports.
|
|||||||
gitignore::Vector{AbstractString}
|
gitignore::Vector{AbstractString}
|
||||||
src::Nullable{AbstractString}
|
src::Nullable{AbstractString}
|
||||||
dest::AbstractString
|
dest::AbstractString
|
||||||
badges::Vector{AbstractString}
|
badges::Vector{Vector{AbstractString}}
|
||||||
view::Dict{String, Any}
|
view::Dict{String, Any}
|
||||||
|
|
||||||
function CodeCov(; config_file::Union{AbstractString, Void}="")
|
function CodeCov(; config_file::Union{AbstractString, Void}="")
|
||||||
@ -26,7 +28,13 @@ Add CodeCov to a template's plugins to enable CodeCov coverage reports.
|
|||||||
["*.jl.cov", "*.jl.*.cov", "*.jl.mem"],
|
["*.jl.cov", "*.jl.*.cov", "*.jl.mem"],
|
||||||
config_file,
|
config_file,
|
||||||
".codecov.yml",
|
".codecov.yml",
|
||||||
["[](https://codecov.io/gh/{{USER}}/{{PKGNAME}}.jl)"],
|
[
|
||||||
|
[
|
||||||
|
"CodeCov",
|
||||||
|
"https://codecov.io/gh/{{USER}}/{{PKGNAME}}.jl/branch/master/graph/badge.svg",
|
||||||
|
"https://codecov.io/gh/{{USER}}/{{PKGNAME}}.jl",
|
||||||
|
],
|
||||||
|
],
|
||||||
Dict{String, Any}(),
|
Dict{String, Any}(),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Coveralls(; config_file::Union{AbstractString, Void}="") -> Coveralls
|
Coveralls(; config_file::Union{AbstractString, Void}="") -> Coveralls
|
||||||
|
|
||||||
Add Coveralls to a template's plugins to enable Coveralls coverage reports.
|
Add `Coveralls` to a template's plugins to optionally add a `.coveralls.yml` configuration
|
||||||
|
file to generated repositories, and an appropriate badge to the README. Also updates the
|
||||||
|
`.gitignore` accordingly.
|
||||||
|
|
||||||
# Keyword Arguments:
|
# Keyword Arguments:
|
||||||
* `config_file::Union{AbstractString, Void}=nothing`: Path to a custom `.coveralls.yml`.
|
* `config_file::Union{AbstractString, Void}=nothing`: Path to a custom `.coveralls.yml`.
|
||||||
@ -11,7 +13,7 @@ Add Coveralls to a template's plugins to enable Coveralls coverage reports.
|
|||||||
gitignore::Vector{AbstractString}
|
gitignore::Vector{AbstractString}
|
||||||
src::Nullable{AbstractString}
|
src::Nullable{AbstractString}
|
||||||
dest::AbstractString
|
dest::AbstractString
|
||||||
badges::Vector{AbstractString}
|
badges::Vector{Vector{AbstractString}}
|
||||||
view::Dict{String, Any}
|
view::Dict{String, Any}
|
||||||
|
|
||||||
function Coveralls(; config_file::Union{AbstractString, Void}=nothing)
|
function Coveralls(; config_file::Union{AbstractString, Void}=nothing)
|
||||||
@ -22,7 +24,13 @@ Add Coveralls to a template's plugins to enable Coveralls coverage reports.
|
|||||||
["*.jl.cov", "*.jl.*.cov", "*.jl.mem"],
|
["*.jl.cov", "*.jl.*.cov", "*.jl.mem"],
|
||||||
config_file,
|
config_file,
|
||||||
".coveralls.yml",
|
".coveralls.yml",
|
||||||
["[](https://coveralls.io/github/{{USER}}/{{PKGNAME}}.jl?branch=master)"],
|
[
|
||||||
|
[
|
||||||
|
"Coveralls",
|
||||||
|
"https://coveralls.io/repos/github/{{USER}}/{{PKGNAME}}.jl/badge.svg?branch=master",
|
||||||
|
"https://coveralls.io/github/{{USER}}/{{PKGNAME}}.jl?branch=master",
|
||||||
|
],
|
||||||
|
],
|
||||||
Dict{String, Any}(),
|
Dict{String, Any}(),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -1,19 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Add a Documenter subtype to a template's plugins to add support for
|
Add a `Documenter` subtype to a template's plugins to add support for documentation
|
||||||
[Documenter.jl](https://github.com/JuliaDocs/Documenter.jl).
|
generation via [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl).
|
||||||
"""
|
"""
|
||||||
abstract type Documenter <: CustomPlugin end
|
abstract type Documenter <: CustomPlugin end
|
||||||
|
|
||||||
"""
|
|
||||||
gen_plugin(plugin::Documenter, template::Template, pkg_name::AbstractString) -> Void
|
|
||||||
|
|
||||||
Generate the "docs" directory with files common to all Documenter subtypes.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
* `plugin::Documenter`: Plugin whose files are being generated.
|
|
||||||
* `template::Template`: Template configuration and plugins.
|
|
||||||
* `pkg_name::AbstractString`: Name of the package.
|
|
||||||
"""
|
|
||||||
function gen_plugin(plugin::Documenter, template::Template, pkg_name::AbstractString)
|
function gen_plugin(plugin::Documenter, template::Template, pkg_name::AbstractString)
|
||||||
if Pkg.installed("Documenter") == nothing
|
if Pkg.installed("Documenter") == nothing
|
||||||
info("Adding Documenter.jl")
|
info("Adding Documenter.jl")
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
GitHubPages(; assets::Vector{AbstractString}=String[]) -> GitHubPages
|
GitHubPages(; assets::Vector{AbstractString}=String[]) -> GitHubPages
|
||||||
|
|
||||||
Add GitHubPages to a template's plugins to add Documenter.jl support via GitHub Pages.
|
Add `GitHubPages` to a template's plugins to add [Documenter](@ref) support via GitHub
|
||||||
|
Pages, including automatic uploading of documentation from [`TravisCI`](@ref). Also
|
||||||
|
adds appropriate badges to the README, and updates the `.gitignore` accordingly.
|
||||||
|
|
||||||
# Keyword Arguments
|
# Keyword Arguments
|
||||||
* `assets::Vector{String}=String[]`: Array of paths to Documenter asset files.
|
* `assets::Vector{String}=String[]`: Array of paths to Documenter asset files.
|
||||||
@ -23,8 +25,8 @@ end
|
|||||||
|
|
||||||
function badges(_::GitHubPages, user::AbstractString, pkg_name::AbstractString)
|
function badges(_::GitHubPages, user::AbstractString, pkg_name::AbstractString)
|
||||||
return [
|
return [
|
||||||
"[](https://$user.github.io/$pkg_name.jl/stable)"
|
badge("Stable", "https://img.shields.io/badge/docs-stable-blue.svg", "https://$user.github.io/$pkg_name.jl/stable")
|
||||||
"[](https://$user.github.io/$pkg_name.jl/latest)"
|
badge("Latest", "https://img.shields.io/badge/docs-latest-blue.svg", "https://$user.github.io/$pkg_name.jl/latest")
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
TravisCI(; config_file::Union{AbstractString, Void}="") -> GenericPlugin
|
TravisCI(; config_file::Union{AbstractString, Void}="") -> GenericPlugin
|
||||||
|
|
||||||
Add TravisCI to a template's plugins to add Travis CI support.
|
Add `TravisCI` to a template's plugins to add a `.travis.yml` configuration file to
|
||||||
|
generated repositories, and an appropriate badge to the README.
|
||||||
|
|
||||||
# Keyword Arguments:
|
# Keyword Arguments:
|
||||||
* `config_file::Union{AbstractString, Void}=""`: Path to a custom `.travis.yml`.
|
* `config_file::Union{AbstractString, Void}=""`: Path to a custom `.travis.yml`.
|
||||||
@ -11,7 +12,7 @@ Add TravisCI to a template's plugins to add Travis CI support.
|
|||||||
gitignore::Vector{AbstractString}
|
gitignore::Vector{AbstractString}
|
||||||
src::Nullable{AbstractString}
|
src::Nullable{AbstractString}
|
||||||
dest::AbstractString
|
dest::AbstractString
|
||||||
badges::Vector{AbstractString}
|
badges::Vector{Vector{AbstractString}}
|
||||||
view::Dict{String, Any}
|
view::Dict{String, Any}
|
||||||
|
|
||||||
function TravisCI(; config_file::Union{AbstractString, Void}="")
|
function TravisCI(; config_file::Union{AbstractString, Void}="")
|
||||||
@ -26,7 +27,13 @@ Add TravisCI to a template's plugins to add Travis CI support.
|
|||||||
[],
|
[],
|
||||||
config_file,
|
config_file,
|
||||||
".travis.yml",
|
".travis.yml",
|
||||||
["[](https://travis-ci.org/{{USER}}/{{PKGNAME}}.jl)"],
|
[
|
||||||
|
[
|
||||||
|
"Build Status",
|
||||||
|
"https://travis-ci.org/{{USER}}/{{PKGNAME}}.jl.svg?branch=master",
|
||||||
|
"https://travis-ci.org/{{USER}}/{{PKGNAME}}.jl",
|
||||||
|
],
|
||||||
|
],
|
||||||
Dict{String, Any}(),
|
Dict{String, Any}(),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,7 @@ using Base.Test
|
|||||||
|
|
||||||
import PkgTemplates: badges, version_floor, substitute, read_license, gen_file, gen_readme,
|
import PkgTemplates: badges, version_floor, substitute, read_license, gen_file, gen_readme,
|
||||||
gen_tests, gen_license, gen_require, gen_entrypoint, gen_gitignore, gen_plugin,
|
gen_tests, gen_license, gen_require, gen_entrypoint, gen_gitignore, gen_plugin,
|
||||||
show_license, LICENSES, LICENSE_DIR
|
show_license, LICENSES, LICENSE_DIR, GenericPlugin
|
||||||
|
|
||||||
mktempdir() do temp_dir
|
mktempdir() do temp_dir
|
||||||
withenv("JULIA_PKGDIR" => temp_dir) do
|
withenv("JULIA_PKGDIR" => temp_dir) do
|
||||||
|
144
test/tests.jl
144
test/tests.jl
@ -1,3 +1,12 @@
|
|||||||
|
struct Foo <: GenericPlugin
|
||||||
|
gitignore::Vector{AbstractString}
|
||||||
|
src::Nullable{AbstractString}
|
||||||
|
dest::AbstractString
|
||||||
|
badges::Vector{Vector{AbstractString}}
|
||||||
|
view::Dict{String, Any}
|
||||||
|
Foo() = new([], @__FILE__, "", [["foo", "bar", "baz"]], Dict{String, Any}())
|
||||||
|
end
|
||||||
|
|
||||||
const git_config = Dict(
|
const git_config = Dict(
|
||||||
"user.name" => "Tester McTestFace",
|
"user.name" => "Tester McTestFace",
|
||||||
"user.email" => "email@web.site",
|
"user.email" => "email@web.site",
|
||||||
@ -11,6 +20,7 @@ template_text = """
|
|||||||
VERSION: {{VERSION}}}
|
VERSION: {{VERSION}}}
|
||||||
{{#DOCUMENTER}}Documenter{{/DOCUMENTER}}
|
{{#DOCUMENTER}}Documenter{{/DOCUMENTER}}
|
||||||
{{#CODECOV}}CodeCov{{/CODECOV}}
|
{{#CODECOV}}CodeCov{{/CODECOV}}
|
||||||
|
{{#CODECOV}}Coveralls{{/CODECOV}}
|
||||||
{{#AFTER}}After{{/AFTER}}
|
{{#AFTER}}After{{/AFTER}}
|
||||||
{{#OTHER}}Other{{/OTHER}}
|
{{#OTHER}}Other{{/OTHER}}
|
||||||
"""
|
"""
|
||||||
@ -70,11 +80,15 @@ write(test_file, template_text)
|
|||||||
|
|
||||||
t = Template(;
|
t = Template(;
|
||||||
user="invenia",
|
user="invenia",
|
||||||
plugins = [GitHubPages(), TravisCI(), AppVeyor(), CodeCov()],
|
plugins = [GitHubPages(), TravisCI(), AppVeyor(), CodeCov(), Coveralls()],
|
||||||
)
|
)
|
||||||
rm(t.temp_dir; recursive=true)
|
rm(t.temp_dir; recursive=true)
|
||||||
@test Set(keys(t.plugins)) == Set([GitHubPages, TravisCI, AppVeyor, CodeCov])
|
@test Set(keys(t.plugins)) == Set(
|
||||||
@test Set(values(t.plugins)) == Set([GitHubPages(), TravisCI(), AppVeyor(), CodeCov()])
|
[GitHubPages, TravisCI, AppVeyor, CodeCov, Coveralls]
|
||||||
|
)
|
||||||
|
@test Set(values(t.plugins)) == Set(
|
||||||
|
[GitHubPages(), TravisCI(), AppVeyor(), CodeCov(), Coveralls()]
|
||||||
|
)
|
||||||
|
|
||||||
@test_warn r".+" t = Template(;
|
@test_warn r".+" t = Template(;
|
||||||
user="invenia",
|
user="invenia",
|
||||||
@ -97,7 +111,11 @@ end
|
|||||||
@test isempty(p.gitignore)
|
@test isempty(p.gitignore)
|
||||||
@test get(p.src) == joinpath(PkgTemplates.DEFAULTS_DIR, "appveyor.yml")
|
@test get(p.src) == joinpath(PkgTemplates.DEFAULTS_DIR, "appveyor.yml")
|
||||||
@test p.dest == ".appveyor.yml"
|
@test p.dest == ".appveyor.yml"
|
||||||
@test p.badges == ["[](https://ci.appveyor.com/project/{{USER}}/{{PKGNAME}}-jl)"]
|
@test p.badges == [[
|
||||||
|
"Build Status",
|
||||||
|
"https://ci.appveyor.com/api/projects/status/github/{{USER}}/{{PKGNAME}}.jl?svg=true",
|
||||||
|
"https://ci.appveyor.com/project/{{USER}}/{{PKGNAME}}-jl",
|
||||||
|
]]
|
||||||
@test isempty(p.view)
|
@test isempty(p.view)
|
||||||
p = AppVeyor(; config_file=nothing)
|
p = AppVeyor(; config_file=nothing)
|
||||||
@test_throws NullException get(p.src)
|
@test_throws NullException get(p.src)
|
||||||
@ -109,7 +127,11 @@ end
|
|||||||
@test isempty(p.gitignore)
|
@test isempty(p.gitignore)
|
||||||
@test get(p.src) == joinpath(PkgTemplates.DEFAULTS_DIR, "travis.yml")
|
@test get(p.src) == joinpath(PkgTemplates.DEFAULTS_DIR, "travis.yml")
|
||||||
@test p.dest == ".travis.yml"
|
@test p.dest == ".travis.yml"
|
||||||
@test p.badges == ["[](https://travis-ci.org/{{USER}}/{{PKGNAME}}.jl)"]
|
@test p.badges == [[
|
||||||
|
"Build Status",
|
||||||
|
"https://travis-ci.org/{{USER}}/{{PKGNAME}}.jl.svg?branch=master",
|
||||||
|
"https://travis-ci.org/{{USER}}/{{PKGNAME}}.jl",
|
||||||
|
]]
|
||||||
@test isempty(p.view)
|
@test isempty(p.view)
|
||||||
p = TravisCI(; config_file=nothing)
|
p = TravisCI(; config_file=nothing)
|
||||||
@test_throws NullException get(p.src)
|
@test_throws NullException get(p.src)
|
||||||
@ -121,7 +143,11 @@ end
|
|||||||
@test p.gitignore == ["*.jl.cov", "*.jl.*.cov", "*.jl.mem"]
|
@test p.gitignore == ["*.jl.cov", "*.jl.*.cov", "*.jl.mem"]
|
||||||
@test get(p.src) == joinpath(PkgTemplates.DEFAULTS_DIR, "codecov.yml")
|
@test get(p.src) == joinpath(PkgTemplates.DEFAULTS_DIR, "codecov.yml")
|
||||||
@test p.dest == ".codecov.yml"
|
@test p.dest == ".codecov.yml"
|
||||||
@test p.badges == ["[](https://codecov.io/gh/{{USER}}/{{PKGNAME}}.jl)"]
|
@test p.badges == [[
|
||||||
|
"CodeCov",
|
||||||
|
"https://codecov.io/gh/{{USER}}/{{PKGNAME}}.jl/branch/master/graph/badge.svg",
|
||||||
|
"https://codecov.io/gh/{{USER}}/{{PKGNAME}}.jl",
|
||||||
|
]]
|
||||||
@test isempty(p.view)
|
@test isempty(p.view)
|
||||||
p = CodeCov(; config_file=nothing)
|
p = CodeCov(; config_file=nothing)
|
||||||
@test_throws NullException get(p.src)
|
@test_throws NullException get(p.src)
|
||||||
@ -133,7 +159,11 @@ end
|
|||||||
@test p.gitignore == ["*.jl.cov", "*.jl.*.cov", "*.jl.mem"]
|
@test p.gitignore == ["*.jl.cov", "*.jl.*.cov", "*.jl.mem"]
|
||||||
@test_throws NullException get(p.src)
|
@test_throws NullException get(p.src)
|
||||||
@test p.dest == ".coveralls.yml"
|
@test p.dest == ".coveralls.yml"
|
||||||
@test p.badges == ["[](https://coveralls.io/github/{{USER}}/{{PKGNAME}}.jl?branch=master)"]
|
@test p.badges == [[
|
||||||
|
"Coveralls",
|
||||||
|
"https://coveralls.io/repos/github/{{USER}}/{{PKGNAME}}.jl/badge.svg?branch=master",
|
||||||
|
"https://coveralls.io/github/{{USER}}/{{PKGNAME}}.jl?branch=master",
|
||||||
|
]]
|
||||||
@test isempty(p.view)
|
@test isempty(p.view)
|
||||||
p = Coveralls(; config_file=nothing)
|
p = Coveralls(; config_file=nothing)
|
||||||
@test_throws NullException get(p.src)
|
@test_throws NullException get(p.src)
|
||||||
@ -176,7 +206,7 @@ end
|
|||||||
user="invenia",
|
user="invenia",
|
||||||
license="MPL",
|
license="MPL",
|
||||||
git_config=git_config,
|
git_config=git_config,
|
||||||
plugins=[TravisCI(), CodeCov(), GitHubPages(), AppVeyor()],
|
plugins=[Coveralls(), TravisCI(), CodeCov(), GitHubPages(), AppVeyor()],
|
||||||
)
|
)
|
||||||
pkg_dir = joinpath(t.temp_dir, test_pkg)
|
pkg_dir = joinpath(t.temp_dir, test_pkg)
|
||||||
|
|
||||||
@ -198,7 +228,14 @@ end
|
|||||||
@test search(readme, "github.io").start <
|
@test search(readme, "github.io").start <
|
||||||
search(readme, "travis").start <
|
search(readme, "travis").start <
|
||||||
search(readme, "appveyor").start <
|
search(readme, "appveyor").start <
|
||||||
search(readme, "codecov").start
|
search(readme, "codecov").start <
|
||||||
|
search(readme, "coveralls").start
|
||||||
|
# Plugins with badges but not in BADGE_ORDER should appear at the far right side.
|
||||||
|
t.plugins[Foo] = Foo()
|
||||||
|
gen_readme(test_pkg, t)
|
||||||
|
readme = readchomp(joinpath(pkg_dir, "README.md"))
|
||||||
|
rm(joinpath(pkg_dir, "README.md"))
|
||||||
|
@test search(readme, "coveralls").start < search(readme, "baz").start
|
||||||
|
|
||||||
@test gen_gitignore(test_pkg, t) == [".gitignore"]
|
@test gen_gitignore(test_pkg, t) == [".gitignore"]
|
||||||
@test isfile(joinpath(pkg_dir, ".gitignore"))
|
@test isfile(joinpath(pkg_dir, ".gitignore"))
|
||||||
@ -326,6 +363,38 @@ end
|
|||||||
p = TravisCI()
|
p = TravisCI()
|
||||||
@test gen_plugin(p, t, test_pkg) == [".travis.yml"]
|
@test gen_plugin(p, t, test_pkg) == [".travis.yml"]
|
||||||
@test isfile(joinpath(pkg_dir, ".travis.yml"))
|
@test isfile(joinpath(pkg_dir, ".travis.yml"))
|
||||||
|
travis = readstring(joinpath(pkg_dir, ".travis.yml"))
|
||||||
|
@test !contains(travis, "after_success")
|
||||||
|
@test !contains(travis, "Codecov.submit")
|
||||||
|
@test !contains(travis, "Coveralls.submit")
|
||||||
|
@test !contains(travis, "Pkg.add(\"Documenter\")")
|
||||||
|
rm(joinpath(pkg_dir, ".travis.yml"))
|
||||||
|
t.plugins[CodeCov] = CodeCov()
|
||||||
|
gen_plugin(p, t, test_pkg)
|
||||||
|
delete!(t.plugins, CodeCov)
|
||||||
|
travis = readstring(joinpath(pkg_dir, ".travis.yml"))
|
||||||
|
@test contains(travis, "after_success")
|
||||||
|
@test contains(travis, "Codecov.submit")
|
||||||
|
@test !contains(travis, "Coveralls.submit")
|
||||||
|
@test !contains(travis, "Pkg.add(\"Documenter\")")
|
||||||
|
rm(joinpath(pkg_dir, ".travis.yml"))
|
||||||
|
t.plugins[Coveralls] = Coveralls()
|
||||||
|
gen_plugin(p, t, test_pkg)
|
||||||
|
delete!(t.plugins, Coveralls)
|
||||||
|
travis = readstring(joinpath(pkg_dir, ".travis.yml"))
|
||||||
|
@test contains(travis, "after_success")
|
||||||
|
@test contains(travis, "Coveralls.submit")
|
||||||
|
@test !contains(travis, "Codecov.submit")
|
||||||
|
@test !contains(travis, "Pkg.add(\"Documenter\")")
|
||||||
|
rm(joinpath(pkg_dir, ".travis.yml"))
|
||||||
|
t.plugins[GitHubPages] = GitHubPages()
|
||||||
|
gen_plugin(p, t, test_pkg)
|
||||||
|
delete!(t.plugins, GitHubPages)
|
||||||
|
travis = readstring(joinpath(pkg_dir, ".travis.yml"))
|
||||||
|
@test contains(travis, "after_success")
|
||||||
|
@test contains(travis, "Pkg.add(\"Documenter\")")
|
||||||
|
@test !contains(travis, "Codecov.submit")
|
||||||
|
@test !contains(travis, "Coveralls.submit")
|
||||||
rm(joinpath(pkg_dir, ".travis.yml"))
|
rm(joinpath(pkg_dir, ".travis.yml"))
|
||||||
p = TravisCI(; config_file=nothing)
|
p = TravisCI(; config_file=nothing)
|
||||||
@test isempty(gen_plugin(p, t, test_pkg))
|
@test isempty(gen_plugin(p, t, test_pkg))
|
||||||
@ -335,6 +404,26 @@ end
|
|||||||
p = AppVeyor()
|
p = AppVeyor()
|
||||||
@test gen_plugin(p, t, test_pkg) == [".appveyor.yml"]
|
@test gen_plugin(p, t, test_pkg) == [".appveyor.yml"]
|
||||||
@test isfile(joinpath(pkg_dir, ".appveyor.yml"))
|
@test isfile(joinpath(pkg_dir, ".appveyor.yml"))
|
||||||
|
appveyor = readstring(joinpath(pkg_dir, ".appveyor.yml"))
|
||||||
|
@test !contains(appveyor, "after_script")
|
||||||
|
@test !contains(appveyor, "Codecov.submit")
|
||||||
|
@test !contains(appveyor, "Coveralls.submit")
|
||||||
|
rm(joinpath(pkg_dir, ".appveyor.yml"))
|
||||||
|
t.plugins[CodeCov] = CodeCov()
|
||||||
|
gen_plugin(p, t, test_pkg)
|
||||||
|
delete!(t.plugins, CodeCov)
|
||||||
|
appveyor = readstring(joinpath(pkg_dir, ".appveyor.yml"))
|
||||||
|
@test contains(appveyor, "after_script")
|
||||||
|
@test contains(appveyor, "Codecov.submit")
|
||||||
|
@test !contains(appveyor, "Coveralls.submit")
|
||||||
|
rm(joinpath(pkg_dir, ".appveyor.yml"))
|
||||||
|
t.plugins[Coveralls] = Coveralls()
|
||||||
|
gen_plugin(p, t, test_pkg)
|
||||||
|
delete!(t.plugins, Coveralls)
|
||||||
|
appveyor = readstring(joinpath(pkg_dir, ".appveyor.yml"))
|
||||||
|
@test contains(appveyor, "after_script")
|
||||||
|
@test contains(appveyor, "Coveralls.submit")
|
||||||
|
@test !contains(appveyor, "Codecov.submit")
|
||||||
rm(joinpath(pkg_dir, ".appveyor.yml"))
|
rm(joinpath(pkg_dir, ".appveyor.yml"))
|
||||||
p = AppVeyor(; config_file=nothing)
|
p = AppVeyor(; config_file=nothing)
|
||||||
@test isempty(gen_plugin(p, t, test_pkg))
|
@test isempty(gen_plugin(p, t, test_pkg))
|
||||||
@ -393,12 +482,25 @@ end
|
|||||||
end
|
end
|
||||||
|
|
||||||
@testset "Mustache substitution" begin
|
@testset "Mustache substitution" begin
|
||||||
|
view = Dict{String, Any}()
|
||||||
|
text = substitute(template_text, view)
|
||||||
|
@test !contains(text, "PKGNAME: $test_pkg")
|
||||||
|
@test !contains(text, "Documenter")
|
||||||
|
@test !contains(text, "CodeCov")
|
||||||
|
@test !contains(text, "Coveralls")
|
||||||
|
@test !contains(text, "After")
|
||||||
|
@test !contains(text, "Other")
|
||||||
|
view["PKGNAME"] = test_pkg
|
||||||
|
view["OTHER"] = true
|
||||||
|
text = substitute(template_text, view)
|
||||||
|
@test contains(text, "PKGNAME: $test_pkg")
|
||||||
|
@test contains(text, "Other")
|
||||||
|
|
||||||
t = Template(; user="invenia")
|
t = Template(; user="invenia")
|
||||||
rm(t.temp_dir; recursive=true)
|
rm(t.temp_dir; recursive=true)
|
||||||
p = Coveralls()
|
view["OTHER"] = false
|
||||||
view = Dict{String, Any}("OTHER" => false)
|
|
||||||
|
|
||||||
text = substitute(template_text, t, test_pkg; view=view)
|
text = substitute(template_text, t; view=view)
|
||||||
@test contains(text, "PKGNAME: $test_pkg")
|
@test contains(text, "PKGNAME: $test_pkg")
|
||||||
@test contains(text, "VERSION: $(t.julia_version.major).$(t.julia_version.minor)")
|
@test contains(text, "VERSION: $(t.julia_version.major).$(t.julia_version.minor)")
|
||||||
@test !contains(text, "Documenter")
|
@test !contains(text, "Documenter")
|
||||||
@ -406,25 +508,25 @@ end
|
|||||||
@test !contains(text, "Other")
|
@test !contains(text, "Other")
|
||||||
|
|
||||||
t.plugins[GitHubPages] = GitHubPages()
|
t.plugins[GitHubPages] = GitHubPages()
|
||||||
text = substitute(template_text, t, test_pkg; view=view)
|
text = substitute(template_text, t; view=view)
|
||||||
@test contains(text, "Documenter")
|
@test contains(text, "Documenter")
|
||||||
@test contains(text, "After")
|
@test contains(text, "After")
|
||||||
empty!(t.plugins)
|
empty!(t.plugins)
|
||||||
|
|
||||||
t.plugins[CodeCov] = CodeCov()
|
t.plugins[CodeCov] = CodeCov()
|
||||||
text = substitute(template_text, t, test_pkg; view=view)
|
text = substitute(template_text, t; view=view)
|
||||||
@test contains(text, "CodeCov")
|
@test contains(text, "CodeCov")
|
||||||
@test contains(text, "After")
|
@test contains(text, "After")
|
||||||
empty!(t.plugins)
|
empty!(t.plugins)
|
||||||
|
|
||||||
|
t.plugins[CodeCov] = Coveralls()
|
||||||
|
text = substitute(template_text, t; view=view)
|
||||||
|
@test contains(text, "Coveralls")
|
||||||
|
@test contains(text, "After")
|
||||||
|
empty!(t.plugins)
|
||||||
|
|
||||||
view["OTHER"] = true
|
view["OTHER"] = true
|
||||||
text = substitute(template_text, t, test_pkg; view=view)
|
text = substitute(template_text, t; view=view)
|
||||||
@test contains(text, "Other")
|
|
||||||
|
|
||||||
text = substitute(template_text, p, test_pkg)
|
|
||||||
@test contains(text, "PKGNAME: $test_pkg")
|
|
||||||
|
|
||||||
text = substitute(template_text, p, test_pkg; view=view)
|
|
||||||
@test contains(text, "Other")
|
@test contains(text, "Other")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user