Skip to content

Instantly share code, notes, and snippets.

@KtorZ
Last active August 18, 2020 06:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KtorZ/0c7411f9bda2db1b3e0ded2ef0c40381 to your computer and use it in GitHub Desktop.
Save KtorZ/0c7411f9bda2db1b3e0ded2ef0c40381 to your computer and use it in GitHub Desktop.
A Makefile for toying around Haskell Program Coverage overlays and stack.
# Haskell Program Coverage - Makefile
#
# This Makefile contains a few top-level build commands for easily making
# code coverage reports using stack and hpc. It is typically used in two
# ways:
#
# a) Locally, to construct an coverage overlay template to purposely ignore
# some part of the source code in the coverage reports.
#
# b) In a continuous integration setup, to generate reports possibly using the
# overlay generated in a).
#
#
# Usage:
#
# a) WORKDIR=.coverage make draft
#
# This will compile and test an existing Haskell project with stack, and create a draft
# overlay report in $WORKDIR. This overlay provides 100% coverage for files tested in the
# Haskell package.
# One would typically use the draft as a baseline and produce a `template.overlay` from it
# to discard some part of the source code from the coverage report. In particular, one may
# discard automatically generated instances (like `Show` or `Eq`) or, terms that never get
# evaluated to WHNF (like `Proxy`) because they only carry information at the type-level.
#
# b) DESTDIR=dist/coverage WORKDIR=.coverage make report
#
# Generates an HPC report for the project, using '$WORKDIR/template.overlay' as a coverage
# overlay. Fails if the file doesn't exists. The report is generated as HTML in $DESTDIR.
#
# b) DESTDIR=dist/coverage make badge
#
# Generates an SVG badge from a given coverage report. The badge can be hosted and shown
# on a project front README and is made using the average coverage between top-level definitions,
# expressions and alternatives.
SHELL = bash
UPSTREAM ?= https://raw.githubusercontent.com/input-output-hk/adrestia/master/.haskell/coverage/Makefile
WORKDIR ?= .coverage
DESTDIR ?= dist/coverage
PKGS = $(shell stack query locals | sed "s@^\s\s.*@@" | sed "s@:@@" | tr '\n' ' ' | sed "s@\s\+@ @g")
OVERLAYS = $(shell echo $(PKGS) | sed "s@\([^ ]*\)@$(WORKDIR)/\1.overlay@g")
DRAFTS = $(shell echo $(PKGS) | sed "s@\([^ ]*\)@$(WORKDIR)/\1.draft@g")
HPC_DIR = $(shell stack path --dist-dir)/hpc
HPC_ROOT = $(shell stack path --local-hpc-root)
.PHONY: badge clean draft report upgrade
### Phony
badge: $(DESTDIR)/badge.svg
@echo -e "Coverage badge generated at: $<"
clean:
stack clean
find . -name *.tix -delete
rm -rf $(HPC_ROOT)/combined/custom/custom.tix
rm -rf $(WORKDIR)/{draft.overlay}
rm -rf $(DESTDIR)/{hpc_index.html,badge.svg}
draft: $(DRAFTS)
@echo -e "Draft overlay generated in: $(WORKDIR)"
report: $(DESTDIR)/hpc_index.html
@echo -e "Report generated at: $<"
upgrade:
$(eval TMP := $(shell mktemp))
wget $(UPSTREAM) -O $(TMP)
@mkdir -p .old
@cp Makefile .old/Makefile
@mv $(TMP) Makefile
@echo "Up-to-date. Old Makefile saved in '.old/Makefile'."
### Recipes
$(DESTDIR)/badge.svg: $(DESTDIR)/hpc_index.html
$(eval COVERAGE := $(shell cat $< | tr '\n' ' ' | sed "s/.*Program Coverage Total.*>\([0-9]\{1,3\}\)%.*>\([0-9]\{1,3\}\)%.*>\([0-9]\{1,3\}\)%.*/\1 \2 \3/"))
$(eval COVERAGE := $(shell echo $(COVERAGE) | awk '{s+=$$1}END{print s/NR}' RS=' '))
$(eval COVERAGE := $(shell LC_NUMERIC=C printf "%.0f" $(COVERAGE)))
$(eval COLOR := $(shell { \
if (( $(COVERAGE) > 80 )); then \
echo "2ecc71"; \
elif (( $(COVERAGE) > 70 )); then \
echo "f1c40f"; \
elif (( $(COVERAGE) > 60 )); then \
echo "e67e22"; \
else \
echo "e74c3c"; \
fi \
}))
@echo '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="144" height="28">' > $@
@echo '<g shape-rendering="crispEdges">' >> $@
@echo '<path fill="#555" d="M0 0h93v28H0z"/>' >> $@
@echo '<path fill="#$(COLOR)" d="M93 0h51v28H93z"/>' >> $@
@echo '</g>' >> $@
@echo '<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="100">' >> $@
@echo '<text x="465" y="175" transform="scale(.1)" textLength="690">COVERAGE</text>' >> $@
@echo '<text x="1185" y="175" font-weight="bold" transform="scale(.1)" textLength="270">$(COVERAGE)%</text>' >> $@
@echo '</g>' >> $@
@echo '</svg>' >> $@
$(DESTDIR)/hpc_index.html: $(HPC_ROOT)/combined/custom/custom.tix
$(eval TMP := $(shell mktemp -d))
for PKG in $(PKGS); do \
OVERLAY=$(WORKDIR)/$$PKG.overlay; \
if [ -f $$OVERLAY ]; then \
HPC=$$(find $(HPC_ROOT)/combined/custom/* -type d -regex ".*$$PKG-[0-9.]+-.*"); \
CABAL=$$(find . -name $$PKG.cabal); \
PKG_HASH=$$(basename $$HPC); \
PKG_SRC=$$(dirname $$CABAL); \
cat $$OVERLAY | sed "s/module \"/module \"$$PKG_HASH\//g" > $(TMP)/$$PKG.overlay; \
stack exec hpc -- overlay --hpcdir=$(HPC_DIR) --srcdir=$$PKG_SRC $(TMP)/$$PKG.overlay > $(WORKDIR)/$$PKG.overlay.tix; \
fi; \
done
@rm -r $(TMP)
stack hpc report --all --destdir $(DESTDIR) $(WORKDIR)/*.tix $<
$(WORKDIR)/%.draft: $(HPC_ROOT)/combined/custom/custom.tix
mkdir -p $(WORKDIR)
$(eval PKG := $(shell basename $@ .draft))
$(eval CABAL := $(shell find . -name $(PKG).cabal))
$(eval PKG_SRC := $(shell dirname $(CABAL)))
stack exec hpc -- draft --hpcdir=$(HPC_DIR) --srcdir=$(PKG_SRC) $< | sed "s/module \".*:/module \"/g" > $@
$(HPC_ROOT)/combined/custom/custom.tix:
stack test --no-terminal --coverage
for PKG in $(PKGS); do \
CABAL=$$(find . -name $$PKG.cabal); \
PKG_SRC=$$(dirname $$CABAL); \
[ ! -f $$PKG_SRC/*.tix ] || mv $$PKG_SRC/*.tix $(WORKDIR); \
done
stack hpc report --all $(WORKDIR)/*.tix
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment