Skip to content

Instantly share code, notes, and snippets.

@torfjelde
Last active June 21, 2023 06:33
Show Gist options
  • Save torfjelde/62c1281d5fc486d3a404e5de6cf285d4 to your computer and use it in GitHub Desktop.
Save torfjelde/62c1281d5fc486d3a404e5de6cf285d4 to your computer and use it in GitHub Desktop.
Script for watching and running tests or certain files (using Revise.jl to avoid full re-compilation). Useful if you're taking a test-driven development. Requries the following packages to installed in the global environment: - `ArgParse` - `Revise` - `TestEnv`
#!/usr/bin/env julia
using ArgParse
# Scenarios:
# 1. Project is specified, but no files => watch project + run `runtests.jl`.
# 2. Project is specified, and files => watch project + run files.
# Command line arguments.
s = ArgParseSettings()
@add_arg_table s begin
"files"
help = "files to both watch and run upon changes"
nargs = '*'
action = :store_arg
"--project"
help = "path to project"
"--setup"
help = "setup script to run before running tests"
"--test"
help = "run the `runtests.jl` file in the project upon changes"
action = :store_true
"--use-testenv"
help = "make use of `TestEnv.activate()`"
action = :store_true
"--not-all"
help = "only run upon changes to the target module"
action = :store_true
"--show-errors"
help = "show errors if encountered"
action = :store_true
"--verbose"
help = "show verbose output"
action = :store_true
end
# Parse command line arguments
parsed_args = parse_args(ARGS, s)
files = parsed_args["files"]
project = parsed_args["project"]
setup = parsed_args["setup"]
istest = parsed_args["test"]
notall = parsed_args["not-all"]
usetestenv = parsed_args["use-testenv"]
verbose = parsed_args["verbose"]
if isempty(files) && !isnothing(project)
@warn "Project specified, but no files; running as if `--test` was specified."
istest = true
end
# Activate the module at path.
using Pkg
if !isnothing(project)
Pkg.activate(project)
end
# Load `Revise`.
using Revise
function revise_watch_and_run(files_to_run, args...; show_errors=false, verbose=false, kwargs...)
# Revise might encounter an error on the files it's watching, in which case
# we need to re-trigger `Revise.entr`. BUT to avoid this happening repeatedly,
# we set `postpone=true` in the `Revise.entr` call above. This postpones the first
# trigger of the provided `f` until an actual change (which should hopefully be fixing
# the error that caused Revise to fail).
revise_errored = false
while true
try
Revise.entr(args...; postpone=revise_errored, kwargs...) do
try
verbose && @info "Detected change; running!"
for f in files_to_run
verbose && @info "Including" abspath(f)
include(abspath(f))
end
catch e
if show_errors
showerror(stderr, e, catch_backtrace())
end
end
# Reset `revise_errored`
revise_errored = false
end
catch e
showerror(stderr, e, catch_backtrace())
# Set `revise_errored` to true, so that the next time `Revise.entr` is called,
# we have `postpone=true` and don't trigger revision until a change is detected.
revise_errored = true
end
end
end
files_to_watch = []
files_to_run = []
modules_to_watch = []
mod_sym = nothing
# Activate the test environment for the package.
if usetestenv
@assert !isnothing(project) "Must specify a project to use `TestEnv`."
@info "Activating test environment."
using TestEnv
# NOTE: Have to get this before `TestEnv.activate`.
mod_sym = Symbol(TestEnv.current_pkg_name())
TestEnv.activate()
end
if istest
@assert !isnothing(project) "Project must be specified if `--test` is specified."
@info "Loading module in $(project)"
# Get the current package name.
if isnothing(mod_sym)
using TestEnv
mod_sym = Symbol(TestEnv.current_pkg_name())
end
# Load the module and assign it to `mod` so we can reference it.
@eval begin
using $mod_sym
mod = $mod_sym
end
# Obtain the path for the `runtests.jl` file.
testpath = joinpath(dirname(pathof(mod)), "..", "test", "runtests.jl")
# Watch the module and run the tests.
push!(files_to_watch, testpath)
push!(files_to_run, testpath)
push!(modules_to_watch, mod)
end
if !isempty(files)
# Watch both module and the specified files, and run the specified files upon changes.
append!(files_to_watch, files)
append!(files_to_run, files)
end
# Watch and run!
isempty(files_to_run) && error("Either `--test` or `files` must be specified.")
# Run the setup script if specified.
if !isnothing(setup)
@info "Including setup script ($setup); to NOT watch modules imported in this too, make sure `--not-all` is specified."
include(abspath(setup))
end
if isempty(modules_to_watch)
revise_watch_and_run(
files_to_run, files_to_watch;
show_errors=parsed_args["show-errors"],
verbose=verbose,
all=!notall,
)
else
revise_watch_and_run(
files_to_run, files_to_watch, modules_to_watch;
show_errors=parsed_args["show-errors"],
verbose=verbose,
all=!notall,
)
end
@torfjelde
Copy link
Author

Typical use-case for me looks something like:

revise-test \
    --verbose \
    --show-errors \
    # specify a setup file, e.g. a copy-paste of the beginning of `test/runtests.jl`
    --setup test/setup.jl \
    # specify the project
    --project . \
    # we want to use `TestEnv.activate()` to activate the test environment
    --use-testenv \
    # specify the files to run when we make changes
    test/mine.jl  # <= usually where I write the tests

@torfjelde
Copy link
Author

Note that this requires having both https://timholy.github.io/Revise.jl/stable/ and https://github.com/JuliaTesting/TestEnv.jl in our global Julia env.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment