Add --filter_sources flag to LcovMerger.

When the `--filter_sources` flag is used `LcovMerger`  excludes from the merged coverage the sources whose names match any of the regex in the flag.

The value of `--filter_sources` is a list of regex separated by `,`.

This flag comes as a preparation for using `gcov` instead of `lcov` for collecting coverage data for C++. The flag was not needed before because `lcov` has some functionality for excluding some files (e.g. `--no-external` to ignore coverage data for system files).

Closes #5834.

PiperOrigin-RevId: 208606867
diff --git a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/BUILD b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/BUILD
index 7af7779..a29d6ac 100644
--- a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/BUILD
+++ b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/BUILD
@@ -104,12 +104,23 @@
 )
 
 java_library(
+    name = "LcovMergerFlags",
+    srcs = ["LcovMergerFlags.java"],
+    deps = [
+        "//third_party:auto_value",
+        "//third_party:guava",
+        "//third_party:jsr305",
+    ],
+)
+
+java_library(
     name = "MainLibrary",
     srcs = ["Main.java"],
     deps = [
         ":Constants",
         ":Coverage",
         ":GcovParser",
+        ":LcovMergerFlags",
         ":LcovParser",
         ":LcovPrinter",
         ":SourceFileCoverage",
diff --git a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Coverage.java b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Coverage.java
index 75eaa42..42e5be5 100644
--- a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Coverage.java
+++ b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Coverage.java
@@ -15,6 +15,7 @@
 package com.google.devtools.lcovmerger;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.TreeMap;
 
 class Coverage {
@@ -45,6 +46,32 @@
     return merged;
   }
 
+  static Coverage filterOutMatchingSources(Coverage coverage, List<String> regexes)
+      throws IllegalArgumentException {
+    if (coverage == null || regexes == null) {
+      throw new IllegalArgumentException("Coverage and regex should not be null.");
+    }
+    if (regexes.isEmpty()) {
+      return coverage;
+    }
+    Coverage filteredCoverage = new Coverage();
+    for (SourceFileCoverage source : coverage.getAllSourceFiles()) {
+      if (!matchesAnyRegex(source.sourceFileName(), regexes)) {
+        filteredCoverage.add(source);
+      }
+    }
+    return filteredCoverage;
+  }
+
+  private static boolean matchesAnyRegex(String input, List<String> regexes) {
+    for (String regex : regexes) {
+      if (input.matches(regex)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   boolean isEmpty() {
     return sourceFiles.isEmpty();
   }
diff --git a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/LcovMergerFlags.java b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/LcovMergerFlags.java
new file mode 100644
index 0000000..c218c53
--- /dev/null
+++ b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/LcovMergerFlags.java
@@ -0,0 +1,82 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.lcovmerger;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import javax.annotation.Nullable;
+
+@AutoValue
+abstract class LcovMergerFlags {
+
+  @Nullable
+  abstract String coverageDir();
+
+  @Nullable
+  abstract String reportsFile();
+
+  abstract String outputFile();
+
+  abstract List<String> filterSources();
+
+  /** Parse flags in the form of "--coverage_dir=... -output_file=..." */
+  static LcovMergerFlags parseFlags(String[] args) {
+    ImmutableList.Builder<String> filterSources = new ImmutableList.Builder<>();
+    String coverageDir = null;
+    String reportsFile = null;
+    String outputFile = null;
+
+    for (String arg : args) {
+      if (!arg.startsWith("--")) {
+        throw new IllegalArgumentException("Argument (" + arg + ") should start with --");
+      }
+      String[] parts = arg.substring(2).split("=", 2);
+      if (parts.length != 2) {
+        throw new IllegalArgumentException("There should be = in argument (" + arg + ")");
+      }
+      switch (parts[0]) {
+        case "coverage_dir":
+          coverageDir = parts[1];
+          break;
+        case "reports_file":
+          reportsFile = parts[1];
+          break;
+        case "output_file":
+          outputFile = parts[1];
+          break;
+        case "filter_sources":
+          filterSources.add(parts[1]);
+          break;
+        default:
+          throw new IllegalArgumentException("Unknown flag " + arg);
+      }
+    }
+
+    if (coverageDir == null && reportsFile == null) {
+      throw new IllegalArgumentException(
+          "At least one of --coverage_dir or --reports_file should be specified.");
+    }
+    if (coverageDir != null && reportsFile != null) {
+      throw new IllegalArgumentException(
+          "Only one of --coverage_dir or --reports_file must be specified.");
+    }
+    if (outputFile == null) {
+      throw new IllegalArgumentException("--output_file was not specified.");
+    }
+    return new AutoValue_LcovMergerFlags(
+        coverageDir, reportsFile, outputFile, filterSources.build());
+  }
+}
diff --git a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Main.java b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Main.java
index b490765..4e3b5da 100644
--- a/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Main.java
+++ b/tools/test/LcovMerger/java/com/google/devtools/lcovmerger/Main.java
@@ -30,9 +30,7 @@
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
@@ -45,17 +43,17 @@
   private static final Logger logger = Logger.getLogger(Main.class.getName());
 
   public static void main(String[] args) {
-    Map<String, String> flags = null;
+    LcovMergerFlags flags = null;
     try {
-      flags = parseFlags(args);
+      flags = LcovMergerFlags.parseFlags(args);
     } catch (IllegalArgumentException e) {
       logger.log(Level.SEVERE, e.getMessage());
       System.exit(1);
     }
 
     List<File> filesInCoverageDir =
-        flags.containsKey("coverage_dir")
-            ? getCoverageFilesInDir(flags.get("coverage_dir"))
+        flags.coverageDir() != null
+            ? getCoverageFilesInDir(flags.coverageDir())
             : Collections.emptyList();
     Coverage coverage =
         Coverage.merge(
@@ -67,8 +65,12 @@
       System.exit(1);
     }
 
+    if (!flags.filterSources().isEmpty()) {
+      coverage = Coverage.filterOutMatchingSources(coverage, flags.filterSources());
+    }
+
     int exitStatus = 0;
-    String outputFile = flags.get("output_file");
+    String outputFile = flags.outputFile();
     try {
       LcovPrinter.print(new FileOutputStream(new File(outputFile)), coverage);
     } catch (IOException e) {
@@ -90,13 +92,12 @@
     return gcovFiles;
   }
 
-  private static List<File> getTracefiles(
-      Map<String, String> flags, List<File> filesInCoverageDir) {
+  private static List<File> getTracefiles(LcovMergerFlags flags, List<File> filesInCoverageDir) {
     List<File> lcovTracefiles = new ArrayList<>();
-    if (flags.containsKey("coverage_dir")) {
+    if (flags.coverageDir() != null) {
       lcovTracefiles = getFilesWithExtension(filesInCoverageDir, TRACEFILE_EXTENSION);
-    } else if (flags.containsKey("reports_file")) {
-      lcovTracefiles = getTracefilesFromFile(flags.get("reports_file"));
+    } else if (flags.reportsFile() != null) {
+      lcovTracefiles = getTracefilesFromFile(flags.reportsFile());
     }
     if (lcovTracefiles.isEmpty()) {
       logger.log(Level.SEVERE, "No lcov file found.");
@@ -170,49 +171,4 @@
     }
     return datFiles;
   }
-
-  /**
-   * Parse flags in the form of "--coverage_dir=... -output_file=..."
-   */
-  private static Map<String, String> parseFlags(String[] args) {
-    Map<String, String> flags = new HashMap<>();
-
-    for (String arg : args) {
-      if (!arg.startsWith("--")) {
-        throw new IllegalArgumentException("Argument (" + arg + ") should start with --");
-      }
-      String[] parts = arg.substring(2).split("=", 2);
-      if (parts.length != 2) {
-        throw new IllegalArgumentException("There should be = in argument (" + arg + ")");
-      }
-      flags.put(parts[0], parts[1]);
-    }
-
-    // Validate flags
-    for (String flag : flags.keySet()) {
-      switch (flag) {
-        case "coverage_dir":
-        case "reports_file":
-        case "output_file":
-          continue;
-        default:
-          throw new IllegalArgumentException("Unknown flag --" + flag);
-      }
-    }
-
-    if (!flags.containsKey("coverage_dir") && !flags.containsKey("reports_file")) {
-      throw new IllegalArgumentException(
-          "At least one of --coverage_dir or --reports_file should be specified.");
-    }
-    if (flags.containsKey("coverage_dir") && flags.containsKey("reports_file")) {
-      throw new IllegalArgumentException(
-          "Only one of --coverage_dir or --reports_file must be specified.");
-    }
-    if (!flags.containsKey("output_file")) {
-      // Different from blaze, this should be mandatory
-      throw new IllegalArgumentException("--output_file was not specified");
-    }
-
-    return flags;
-  }
 }
diff --git a/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/BUILD b/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/BUILD
index 66df9fe..97de49e 100644
--- a/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/BUILD
+++ b/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/BUILD
@@ -103,6 +103,16 @@
 )
 
 java_test(
+    name = "LcovMergerFlagsTest",
+    srcs = ["LcovMergerFlagsTest.java"],
+    deps = [
+        "//third_party:junit4",
+        "//third_party:truth",
+        "//tools/test/LcovMerger/java/com/google/devtools/lcovmerger:LcovMergerFlags",
+    ],
+)
+
+java_test(
     name = "MainTest",
     srcs = ["MainTest.java"],
     deps = [
diff --git a/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/CoverageTest.java b/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/CoverageTest.java
index 724a209..56d2e6b 100644
--- a/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/CoverageTest.java
+++ b/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/CoverageTest.java
@@ -22,7 +22,9 @@
 import static com.google.devtools.lcovmerger.LcovMergerTestUtils.createSourceFile1;
 import static com.google.devtools.lcovmerger.LcovMergerTestUtils.createSourceFile2;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import java.util.Collection;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -78,4 +80,92 @@
     assertTracefile1(Iterables.get(coverage.getAllSourceFiles(), 0));
     assertTracefile1(Iterables.get(coverage.getAllSourceFiles(), 1));
   }
+
+  @Test
+  public void testFilterSources() {
+    Coverage coverage = new Coverage();
+
+    coverage.add(new SourceFileCoverage("/filterOut/package/file1.c"));
+    coverage.add(new SourceFileCoverage("/filterOut/package/file2.c"));
+    SourceFileCoverage validSource1 = new SourceFileCoverage("/valid/package/file3.c");
+    coverage.add(validSource1);
+    SourceFileCoverage validSource2 = new SourceFileCoverage("/valid/package/file4.c");
+    coverage.add(validSource2);
+    Collection<SourceFileCoverage> filteredSources =
+        Coverage.filterOutMatchingSources(coverage, ImmutableList.of("/filterOut/package/.+"))
+            .getAllSourceFiles();
+
+    assertThat(filteredSources).containsExactly(validSource1, validSource2);
+  }
+
+  @Test
+  public void testFilterSourcesEmptyResult() {
+    Coverage coverage = new Coverage();
+
+    coverage.add(new SourceFileCoverage("/filterOut/package/file1.c"));
+    coverage.add(new SourceFileCoverage("/filterOut/package/file2.c"));
+    Collection<SourceFileCoverage> filteredSources =
+        Coverage.filterOutMatchingSources(coverage, ImmutableList.of("/filterOut/package/.+"))
+            .getAllSourceFiles();
+
+    assertThat(filteredSources).isEmpty();
+  }
+
+  @Test
+  public void testFilterSourcesNoMatches() {
+    Coverage coverage = new Coverage();
+
+    SourceFileCoverage validSource1 = new SourceFileCoverage("/valid/package/file3.c");
+    coverage.add(validSource1);
+    SourceFileCoverage validSource2 = new SourceFileCoverage("/valid/package/file4.c");
+    coverage.add(validSource2);
+    Collection<SourceFileCoverage> filteredSources =
+        Coverage.filterOutMatchingSources(coverage, ImmutableList.of("/something/else/.+"))
+            .getAllSourceFiles();
+
+    assertThat(filteredSources).containsExactly(validSource1, validSource2);
+  }
+
+  @Test
+  public void testFilterSourcesMultipleRegex() {
+    Coverage coverage = new Coverage();
+
+    coverage.add(new SourceFileCoverage("/filterOut/package/file1.c"));
+    coverage.add(new SourceFileCoverage("/filterOut/package/file2.c"));
+    coverage.add(new SourceFileCoverage("/repo/external/p.c"));
+    SourceFileCoverage validSource1 = new SourceFileCoverage("/valid/package/file3.c");
+    coverage.add(validSource1);
+    SourceFileCoverage validSource2 = new SourceFileCoverage("/valid/package/file4.c");
+    coverage.add(validSource2);
+    Collection<SourceFileCoverage> filteredSources =
+        Coverage.filterOutMatchingSources(
+                coverage, ImmutableList.of("/filterOut/package/.+", ".+external.+"))
+            .getAllSourceFiles();
+
+    assertThat(filteredSources).containsExactly(validSource1, validSource2);
+  }
+
+  @Test
+  public void testFilterSourcesNoFilter() {
+    Coverage coverage = new Coverage();
+
+    SourceFileCoverage validSource1 = new SourceFileCoverage("/valid/package/file3.c");
+    coverage.add(validSource1);
+    SourceFileCoverage validSource2 = new SourceFileCoverage("/valid/package/file4.c");
+    coverage.add(validSource2);
+    Collection<SourceFileCoverage> filteredSources =
+        Coverage.filterOutMatchingSources(coverage, ImmutableList.of()).getAllSourceFiles();
+
+    assertThat(filteredSources).containsExactly(validSource1, validSource2);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFilterSourcesNullCoverage() {
+    Coverage.filterOutMatchingSources(null, ImmutableList.of());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFilterSourcesNullRegex() {
+    Coverage.filterOutMatchingSources(new Coverage(), null);
+  }
 }
diff --git a/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/LcovMergerFlagsTest.java b/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/LcovMergerFlagsTest.java
new file mode 100644
index 0000000..47ff4ef
--- /dev/null
+++ b/tools/test/LcovMerger/javatests/com/google/devtools/lcovmerger/LcovMergerFlagsTest.java
@@ -0,0 +1,121 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.lcovmerger;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class LcovMergerFlagsTest {
+  @Test
+  public void parseFlagsTestCoverageDirOutputFile() {
+    LcovMergerFlags flags =
+        LcovMergerFlags.parseFlags(
+            new String[] {
+              "--coverage_dir=my_dir", "--output_file=my_file",
+            });
+    assertThat(flags.coverageDir()).isEqualTo("my_dir");
+    assertThat(flags.outputFile()).isEqualTo("my_file");
+    assertThat(flags.reportsFile()).isNull();
+    assertThat(flags.filterSources()).isEmpty();
+  }
+
+  @Test
+  public void parseFlagsTestReportsFileOutputFile() {
+    LcovMergerFlags flags =
+        LcovMergerFlags.parseFlags(
+            new String[] {
+              "--reports_file=my_reports_file", "--output_file=my_file",
+            });
+    assertThat(flags.reportsFile()).isEqualTo("my_reports_file");
+    assertThat(flags.outputFile()).isEqualTo("my_file");
+    assertThat(flags.coverageDir()).isNull();
+    assertThat(flags.filterSources()).isEmpty();
+  }
+
+  @Test
+  public void parseFlagsTestReportsFileOutputFileFilterSources() {
+    LcovMergerFlags flags =
+        LcovMergerFlags.parseFlags(
+            new String[] {
+              "--reports_file=my_reports_file",
+              "--output_file=my_file",
+              "--filter_sources=first_filter"
+            });
+    assertThat(flags.reportsFile()).isEqualTo("my_reports_file");
+    assertThat(flags.outputFile()).isEqualTo("my_file");
+    assertThat(flags.coverageDir()).isNull();
+    assertThat(flags.filterSources()).containsExactly("first_filter");
+  }
+
+  @Test
+  public void parseFlagsTestReportsFileOutputFileMultipleFilterSources() {
+    LcovMergerFlags flags =
+        LcovMergerFlags.parseFlags(
+            new String[] {
+              "--reports_file=my_reports_file",
+              "--output_file=my_file",
+              "--filter_sources=first_filter",
+              "--filter_sources=second_filter"
+            });
+    assertThat(flags.reportsFile()).isEqualTo("my_reports_file");
+    assertThat(flags.outputFile()).isEqualTo("my_file");
+    assertThat(flags.coverageDir()).isNull();
+    assertThat(flags.filterSources()).containsExactly("first_filter", "second_filter");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseFlagsTestCoverageDirAndReportsFile() {
+    LcovMergerFlags.parseFlags(
+        new String[] {"--reports_file=my_reports_file", "--coverage_dir=my_coverage_dir"});
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseFlagsTestEmptyFlags() {
+    LcovMergerFlags.parseFlags(new String[] {});
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseFlagsTestNoOutputFile() {
+    LcovMergerFlags.parseFlags(
+        new String[] {
+          "--reports_file=my_reports_file",
+        });
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseFlagsTestUnknownFlag() {
+    LcovMergerFlags.parseFlags(
+        new String[] {
+          "--fake_flag=my_reports_file",
+        });
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseFlagsTestInvalidFlagValue() {
+    LcovMergerFlags.parseFlags(
+        new String[] {
+          "--reports_file", "--output_file=my_file",
+        });
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseFlagsTestInvalidFlagValueWithoutDashes() {
+    LcovMergerFlags.parseFlags(
+        new String[] {
+          "reports_file", "--output_file=my_file",
+        });
+  }
+}