Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@ jobs:
println(io, "R_APT_VERSION=", pins["runtimes"]["r"]["apt_version"])
println(io, "BUN_VERSION=", pins["toolchain"]["bun"]["version"])
println(io, "CUTADAPT_SPEC=cutadapt==", pins["tools"]["cutadapt"]["version"])
println(io, "MULTIQC_SPEC=multiqc==", pins["tools"]["multiqc"]["version"])
# FastQC ships one platform-neutral zip, so its archive hangs off the
# "any" key rather than a per-platform one, and it cannot join the loop
# below. test_install_pins.jl asserts that shape, so a pin file that
# grew a linux-x86_64 fastqc entry would fail there before it failed here.
fastqc = pins["tools"]["fastqc"]["archives"]["any"]
println(io, "FASTQC_VERSION=", pins["tools"]["fastqc"]["version"])
println(io, "FASTQC_URL=", fastqc["url"])
println(io, "FASTQC_SHA256=", fastqc["sha256"])
for tool in ("vsearch", "swarm")
archive = pins["tools"][tool]["archives"]["linux-x86_64"]
println(io, uppercase(tool), "_VERSION=", pins["tools"][tool]["version"])
Expand Down Expand Up @@ -323,6 +332,25 @@ jobs:
rm -rf swarm.tar.gz "swarm-$SWARM_VERSION-linux-x86_64"
swarm --version

- name: Install multiqc
run: pip install "$MULTIQC_SPEC"

# FastQC is a Perl launcher round a Java jar, so it is installed as a directory
# rather than a single binary: the launcher resolves its jars relative to its own
# location, and moving it out of FastQC/ breaks it. Unpacked to /opt and reached
# through a symlink, which is what config/ci/tools.yml means by `path: "fastqc"`.
# The launcher runs `java` off PATH, so `fastqc --version` proves both that the
# install landed and that a JRE is present on the runner.
- name: Install fastqc
run: |
curl -sSL -o fastqc.zip "$FASTQC_URL"
echo "$FASTQC_SHA256 fastqc.zip" | sha256sum -c -
sudo unzip -q -d /opt fastqc.zip
rm -f fastqc.zip
sudo chmod +x /opt/FastQC/fastqc
sudo ln -sf /opt/FastQC/fastqc /usr/local/bin/fastqc
fastqc --version

- name: Instantiate Julia project
run: julia --project=. -e 'import Pkg; Pkg.instantiate()'

Expand Down
21 changes: 20 additions & 1 deletion test/integration/test_pipeline.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
if isdir(test_study_dir)
for entry in readdir(test_study_dir; join=true)
isdir(entry) || continue
for subdir in ("cutadapt", "dada2", "swarm", "vsearch", "merged")
for subdir in ("QC", "cutadapt", "dada2", "swarm", "vsearch", "merged")
d = joinpath(entry, subdir)
isdir(d) && rm(d; recursive=true)
end
Expand Down Expand Up @@ -103,6 +103,25 @@
continue
end

# Stage 0: FastQC/MultiQC over the raw reads. The server runs this as an
# independent stage rather than a link in the chain, so nothing downstream
# consumes it -- which is exactly why it needs asserting here. Until issue #30
# CI installed neither tool, so the whole prefilter QC stage, its flag plumbing
# out of pipeline.yml and its provenance capture had no coverage at all.
multiqc(project)
qc_dir = joinpath(project.dir, "QC")
@test isfile(joinpath(qc_dir, "multiqc_report.html"))
for logname in ("fastqc.log", "multiqc.log")
p = joinpath(qc_dir, "logs", logname)
@test isfile(p)
# Non-empty AND carrying the invocation marker. An empty log is what a stage
# that made its log directory and then died leaves behind, and it would
# satisfy `isfile` alone; the marker line is the provenance record itself,
# the same one the DADA2 assertions below grep for.
@test filesize(p) > 0
@test any(l -> startswith(l, "[MetaManifold] cmd: "), readlines(p))
end

# Stage 1: cutadapt
trimmed = cutadapt(project)
@test isdir(trimmed.dir)
Expand Down
36 changes: 35 additions & 1 deletion test/unit/test_install_pins.jl
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,43 @@ end
@test occursin("tool_versions.yml", runs)
@test occursin("sha256sum -c", runs)

for tool in ("vsearch", "swarm")
# fastqc and multiqc joined this list with issue #30. Leaving them out would
# have made the guard vacuous for exactly the two tools it had just been
# extended to cover -- the shape that let the gap open in the first place.
for tool in ("vsearch", "swarm", "fastqc", "multiqc")
@test !occursin(pins["tools"][tool]["version"], runs)
end

# And the gap itself: every tool the pipeline shells out to must actually be
# installed by the workflow, which is what issue #30 found missing for fastqc
# and multiqc.
#
# The assertion is on the DEREFERENCE of each exported pin, not on the tool's
# name. Written the obvious way -- does "fastqc" appear anywhere in the run
# blocks -- this guard passes with the install step deleted, because the
# pin-export block above mentions every tool by name whether or not anything
# installs it. That version was written here, and deleting the fastqc step did
# not redden it. `$FASTQC_URL` appears only where a step consumes the pin.
# raw"..." rather than "\$...": the string is a shell variable reference to be
# found verbatim in the YAML, never a Julia interpolation, and config/ci/lint_source.jl
# rejects an escaped `\$` in an interpolating string precisely to force that
# distinction to be stated. The two forms carry the same bytes; only one says why.
for (tool, ref) in (("cutadapt", raw"$CUTADAPT_SPEC"),
("multiqc", raw"$MULTIQC_SPEC"),
("fastqc", raw"$FASTQC_URL"),
("vsearch", raw"$VSEARCH_URL"),
("swarm", raw"$SWARM_URL"))
@test occursin(ref, runs)
end

# The two archive tools that are not a single binary still have to end up on
# PATH, because config/ci/tools.yml resolves every tool by bare name.
for tool in ("vsearch", "swarm", "fastqc")
@test occursin("/usr/local/bin/$tool", runs)
end

# cd-hit is the one tool CI does not pin, so it is matched on the apt line.
@test occursin("apt-get install -y cd-hit", runs)
end

@testset "a required check name is a stable identifier" begin
Expand Down
37 changes: 29 additions & 8 deletions test/unit/test_provenance.jl
Original file line number Diff line number Diff line change
Expand Up @@ -424,16 +424,37 @@ canned(out::String, err::String = "", code::Int = 0) =
## Live probes. Gated, so that the suite passes on a machine with neither the
# binaries nor the R packages installed.
@testset "live probes of what is actually installed" begin
# The gate is not the same question on CI as on a laptop. ci.yml installs every
# tool in TOOL_PROBES, so an absent binary there is a provisioning regression;
# on a bare checkout it is expected. Issue #30 was this loop skipping fastqc and
# multiqc quietly for months because CI installed neither: an `@info` scrolls
# past, and a zero-failure count says nothing about which probes ran at all.
tools_must_be_present = get(ENV, "CI", "false") == "true"

for (key, probe) in PV.TOOL_PROBES
bin = PV.default_bin_resolver(key)
if isnothing(Sys.which(bin))
@info "Provenance: skipping live probe of $key, which is not installed"
continue
# One testset per tool, so the suite's own summary names all six and shows
# per tool whether it ran, was skipped, or failed. That naming is the
# assertion; a flat loop can only report the tools it did not skip.
@testset "$key" begin
bin = PV.default_bin_resolver(key)
resolved = Sys.which(bin)

if tools_must_be_present
# Named by the enclosing testset, so a red line reads "fastqc".
@test resolved !== nothing
end

if isnothing(resolved)
# Broken, not passed: it shows in the summary's own column rather
# than disappearing into the count of things that went fine.
@test_skip PV.probe_tool(probe)
else
record = PV.probe_tool(probe)
@test !isempty(record.version)
@test isabspath(record.path)
@test length(record.sha256) == 64
end
end
record = PV.probe_tool(probe)
@test !isempty(record.version)
@test isabspath(record.path)
@test length(record.sha256) == 64
end

r_ready = try
Expand Down
Loading