diff --git a/.gitignore b/.gitignore
index 82c4583d8c90e9ab70dec1fe990abb955bfd8ad6..d134aa6203574403a62970fc655bb794b21e36d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@
 /src/NeuralNetworks.ipynb
 /examples/random_systems.ipynb
 /examples/ToolboxExamples.ipynb
+/test/coverage.ipynb
diff --git a/Project.toml b/Project.toml
index bedc1b330e0a327c118595b87416b8489dadc604..d6a2ea830bdd3f6d491e0f5739a9631c785da8c7 100644
--- a/Project.toml
+++ b/Project.toml
@@ -6,6 +6,7 @@ version = "0.1.1"
 [deps]
 BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e"
 ControlSystems = "a6e380b2-a6ca-5380-bf3e-84a91bcd477e"
+Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037"
 Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"
 Hypatia = "b99e6be6-89ff-11e8-14f8-45c827f4f8f2"
 IntervalArithmetic = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253"
@@ -18,6 +19,7 @@ MosekTools = "1ec41992-ff65-5c91-ac43-2df89e9693a4"
 NNlib = "872c559c-99b0-510c-b3b7-b6c96a88d5cd"
 Optim = "429524aa-4258-5aef-a3af-852621145aeb"
 OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
+Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
 Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
 Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45"
 Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
diff --git a/test/coverage.jl b/test/coverage.jl
new file mode 100644
index 0000000000000000000000000000000000000000..b04426713775450fe887e55b2a96b0e31f7cbe78
--- /dev/null
+++ b/test/coverage.jl
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# ---
+# jupyter:
+#   jupytext:
+#     text_representation:
+#       extension: .jl
+#       format_name: percent
+#       format_version: '1.3'
+#       jupytext_version: 1.16.4
+#   kernelspec:
+#     display_name: Julia 1.10.2
+#     language: julia
+#     name: julia-1.10
+# ---
+
+# %% [markdown]
+# # Prelude
+
+# %%
+import Pkg
+Pkg.activate("..")
+Pkg.instantiate()
+
+# %%
+using Coverage
+using Base.Filesystem
+
+# %% [markdown]
+# # Run tests, Collecting stats
+
+# %%
+for (d, ds, fs) in walkdir("../src")
+    for f in fs
+        if endswith(f, ".cov")
+            rm("$(d)/$(f)")
+        end
+    end
+end
+
+# %%
+Pkg.test("IQC"; coverage=true)
+
+# %%
+coverage = process_folder("../src") # defaults to src/; alternatively, supply the folder name as argument
+coverage = [ c for c in coverage if !occursin("ipynb_checkpoints", c.filename) ];
+
+# %% [markdown]
+# # Per file summary statistics
+
+# %%
+for cov in coverage
+    good = length([ c for c in cov.coverage if !isnothing(c) && !iszero(c) ])
+    total = length([ c for c in cov.coverage if !isnothing(c) ])
+    println("$(split(cov.filename, "/")[end]): $(good)/$(total)")
+end
+
+# %% [markdown]
+# # Untested Functions and Macros
+
+# %%
+re_macro = r"^macro .*"
+re_function = r"^function .*"
+re_function_short = r"^[A-Za-z0-9z.]*\(.*\).*="
+
+# %%
+for cov in coverage
+    println("File: $(cov.filename)")
+    for (c, ℓ) in zip(cov.coverage, split(cov.source, "\n"))
+        if isnothing(c) || iszero(c)
+            if occursin(re_macro, ℓ) || occursin(re_function, ℓ) || occursin(re_function_short, ℓ)
+                println(ℓ)
+            end
+        end
+    end
+end
+
+# %%
+
+# %%