refactor: build the coverage test rules from one factory (#2952)

The two rules differ only in which merger they default _lcov_merger to,
which has to stay a private attribute, so each merger still needs its
own rule.

### Changes are visible to end-users: no

### Test plan

- Covered by existing test cases
diff --git a/js/private/test/coverage/test.bzl b/js/private/test/coverage/test.bzl
index 1ad8556..d8b3b9d 100644
--- a/js/private/test/coverage/test.bzl
+++ b/js/private/test/coverage/test.bzl
@@ -2,28 +2,20 @@
 
 load("//js/private:js_binary.bzl", "js_binary_lib")
 
-coverage_fail_test = rule(
-    implementation = js_binary_lib.implementation,
-    attrs = dict(js_binary_lib.attrs, **{
-        "_lcov_merger": attr.label(
-            executable = True,
-            default = Label("//js/private/test/coverage:fail_merger"),
-            cfg = "exec",
-        ),
-    }),
-    test = True,
-    toolchains = js_binary_lib.toolchains,
-)
+# _lcov_merger must be a private attribute, so each merger needs its own rule.
+def _coverage_test(merger):
+    return rule(
+        implementation = js_binary_lib.implementation,
+        attrs = dict(js_binary_lib.attrs, **{
+            "_lcov_merger": attr.label(
+                executable = True,
+                default = Label("//js/private/test/coverage:" + merger),
+                cfg = "exec",
+            ),
+        }),
+        test = True,
+        toolchains = js_binary_lib.toolchains,
+    )
 
-coverage_pass_test = rule(
-    implementation = js_binary_lib.implementation,
-    attrs = dict(js_binary_lib.attrs, **{
-        "_lcov_merger": attr.label(
-            executable = True,
-            default = Label("//js/private/test/coverage:pass_merger"),
-            cfg = "exec",
-        ),
-    }),
-    test = True,
-    toolchains = js_binary_lib.toolchains,
-)
+coverage_fail_test = _coverage_test("fail_merger")
+coverage_pass_test = _coverage_test("pass_merger")