diff --git a/.circleci/config.yml b/.circleci/config.yml
index 782aaeb9bce52f9dcb8855261dd0b56fc75e1729..59e7173f77276ccce39a782e7a38da063816d8bd 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -201,7 +201,13 @@ jobs:
             #
             # Further, we want the "doc" output built as well because that's
             # where the coverage data ends up.
-            nix-build tests.nix \
+            #
+            # Also explicitly specify the number of cores to use such that it
+            # only slightly exceeds what CircleCI advertises for the resource
+            # class (defined above) we're using.  The CircleCI environment
+            # looks like it has many more cores than are actually usable by
+            # our build.
+            nix-build --cores 5 tests.nix \
               --argstr hypothesisProfile ci \
               --arg collectCoverage true \
               --argstr tahoe-lafs-source << parameters.tahoe-lafs-source >> \
diff --git a/tests.nix b/tests.nix
index a3bf6878d70864224deedd1c0290d6ec9200a575..3145d2924c5f2d172379f8ccfd520349cc7ace0e 100644
--- a/tests.nix
+++ b/tests.nix
@@ -21,43 +21,61 @@ let
     inherit (privatestorage) pkgs mach-nix zkapauthorizer;
     inherit (pkgs) lib;
     hypothesisProfile' = if hypothesisProfile == null then "default" else hypothesisProfile;
-    defaultTrialArgs = [ "--rterrors" ] ++ (lib.optional (! collectCoverage) "--jobs=$(($NIX_BUILD_CORES > 8 ? 8 : $NIX_BUILD_CORES))");
+    defaultTrialArgs = [ "--rterrors" "--jobs=$NIX_BUILD_CORES" ];
     trialArgs' = if trialArgs == null then defaultTrialArgs else trialArgs;
     extraTrialArgs = builtins.concatStringsSep " " trialArgs';
     testSuite' = if testSuite == null then "_zkapauthorizer" else testSuite;
 
+    coveragerc = builtins.path {
+      name = "coveragerc";
+      path = ./.coveragerc;
+    };
+    coverage-env = lib.optionalString collectCoverage "COVERAGE_PROCESS_START=${coveragerc}";
+    coverage-cmd = lib.optionalString collectCoverage "coverage run --debug=config --rcfile=${coveragerc} --module";
+
     python = mach-nix.mkPython {
       inherit (zkapauthorizer.meta.mach-nix) python providers;
-      requirements =
-        builtins.readFile ./requirements/test.in;
+      requirements = ''
+        ${builtins.readFile ./requirements/test.in}
+        ${if collectCoverage then "coverage_enable_subprocess" else ""}
+      '';
       packagesExtra = [ zkapauthorizer ];
       _.hypothesis.postUnpack = "";
     };
 
-    tests = pkgs.runCommand "zkapauthorizer-tests" {
+    lint = pkgs.runCommand "zkapauthorizer-lint" {
       passthru = {
         inherit python;
       };
     } ''
-      mkdir -p $out
-
       pushd ${zkapauthorizer.src}
       ${python}/bin/flake8 src
       popd
 
-      ZKAPAUTHORIZER_HYPOTHESIS_PROFILE=${hypothesisProfile'} ${python}/bin/python -m ${if collectCoverage
-        then "coverage run --debug=config --rcfile=${zkapauthorizer.src}/.coveragerc --module"
-        else ""
-      } twisted.trial ${extraTrialArgs} ${testSuite'}
+      touch $out
+      '';
+
+    tests = pkgs.runCommand "zkapauthorizer-tests" {
+      passthru = {
+        inherit python;
+      };
+    } ''
+      mkdir -p $out
+
+      export ZKAPAUTHORIZER_HYPOTHESIS_PROFILE=${hypothesisProfile'}
+      ${coverage-env} ${python}/bin/python -m ${coverage-cmd} twisted.trial ${extraTrialArgs} ${testSuite'}
 
       ${lib.optionalString collectCoverage
         ''
           mkdir -p "$out/coverage"
           cp -v .coverage.* "$out/coverage"
+          ${python}/bin/python -m coverage combine
+          cp -v .coverage "$out/coverage"
+          ${python}/bin/python -m coverage html -d "$out/htmlcov"
         ''
       }
     '';
 in
 {
-  inherit privatestorage tests;
+  inherit privatestorage lint tests;
 }