Add recursive pattern matching for subfolders in file_enumerator.

FolderSearchPolicy parameter is added to FileEnumerator class.
MATCH_ONLY policy is default and refers to the current behaviour where
the pattern only mathces the contents of root_path, not files in
recursive subdirectories. ALL policy is the new fixed behaviour. It
matches pattern to all subdirecroties.

CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.win:win10_chromium_x64_rel_ng

Review-Url: https://codereview.chromium.org/2892173003
Cr-Commit-Position: refs/heads/master@{#483920}


CrOS-Libchrome-Original-Commit: 829f2e0b37d0175ac65aee0cbc9a603fcc994db7
diff --git a/base/files/file_enumerator.cc b/base/files/file_enumerator.cc
index 9749980..dfa277a 100644
--- a/base/files/file_enumerator.cc
+++ b/base/files/file_enumerator.cc
@@ -18,4 +18,9 @@
           !(INCLUDE_DOT_DOT & file_type_));
 }
 
+bool FileEnumerator::IsTypeMatched(bool is_dir) const {
+  return (file_type_ &
+          (is_dir ? FileEnumerator::DIRECTORIES : FileEnumerator::FILES)) != 0;
+}
+
 }  // namespace base
diff --git a/base/files/file_enumerator.h b/base/files/file_enumerator.h
index 7cac8dd..4f3ee57 100644
--- a/base/files/file_enumerator.h
+++ b/base/files/file_enumerator.h
@@ -84,6 +84,17 @@
 #endif
   };
 
+  // Search policy for intermediate folders.
+  enum class FolderSearchPolicy {
+    // Recursive search will pass through folders whose names match the
+    // pattern. Inside each one, all files will be returned. Folders with names
+    // that do not match the pattern will be ignored within their interior.
+    MATCH_ONLY,
+    // Recursive search will pass through every folder and perform pattern
+    // matching inside each one.
+    ALL,
+  };
+
   // |root_path| is the starting directory to search for. It may or may not end
   // in a slash.
   //
@@ -101,9 +112,6 @@
   // since the underlying code uses OS-specific matching routines.  In general,
   // Windows matching is less featureful than others, so test there first.
   // If unspecified, this will match all files.
-  // NOTE: the pattern only matches the contents of root_path, not files in
-  // recursive subdirectories.
-  // TODO(erikkay): Fix the pattern matching to work at all levels.
   FileEnumerator(const FilePath& root_path,
                  bool recursive,
                  int file_type);
@@ -111,6 +119,11 @@
                  bool recursive,
                  int file_type,
                  const FilePath::StringType& pattern);
+  FileEnumerator(const FilePath& root_path,
+                 bool recursive,
+                 int file_type,
+                 const FilePath::StringType& pattern,
+                 FolderSearchPolicy folder_search_policy);
   ~FileEnumerator();
 
   // Returns the next file or an empty string if there are no more results.
@@ -127,28 +140,27 @@
   // Returns true if the given path should be skipped in enumeration.
   bool ShouldSkip(const FilePath& path);
 
+  bool IsTypeMatched(bool is_dir) const;
+
+  bool IsPatternMatched(const FilePath& src) const;
+
 #if defined(OS_WIN)
   // True when find_data_ is valid.
-  bool has_find_data_;
+  bool has_find_data_ = false;
   WIN32_FIND_DATA find_data_;
-  HANDLE find_handle_;
+  HANDLE find_handle_ = INVALID_HANDLE_VALUE;
 #elif defined(OS_POSIX)
-
-  // Read the filenames in source into the vector of DirectoryEntryInfo's
-  static bool ReadDirectory(std::vector<FileInfo>* entries,
-                            const FilePath& source, bool show_links);
-
   // The files in the current directory
   std::vector<FileInfo> directory_entries_;
 
   // The next entry to use from the directory_entries_ vector
   size_t current_directory_entry_;
 #endif
-
   FilePath root_path_;
-  bool recursive_;
-  int file_type_;
-  FilePath::StringType pattern_;  // Empty when we want to find everything.
+  const bool recursive_;
+  const int file_type_;
+  FilePath::StringType pattern_;
+  const FolderSearchPolicy folder_search_policy_;
 
   // A stack that keeps track of which subdirectories we still need to
   // enumerate in the breadth-first search.
diff --git a/base/files/file_enumerator_posix.cc b/base/files/file_enumerator_posix.cc
index a1b4949..bc5c3cc 100644
--- a/base/files/file_enumerator_posix.cc
+++ b/base/files/file_enumerator_posix.cc
@@ -8,12 +8,29 @@
 #include <errno.h>
 #include <fnmatch.h>
 #include <stdint.h>
+#include <string.h>
 
 #include "base/logging.h"
 #include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
 
 namespace base {
+namespace {
+
+void GetStat(const FilePath& path, bool show_links, struct stat* st) {
+  DCHECK(st);
+  const int res = show_links ? lstat(path.value().c_str(), st)
+                             : stat(path.value().c_str(), st);
+  if (res < 0) {
+    // Print the stat() error message unless it was ENOENT and we're following
+    // symlinks.
+    if (!(errno == ENOENT && !show_links))
+      DPLOG(ERROR) << "Couldn't stat" << path.value();
+    memset(st, 0, sizeof(*st));
+  }
+}
+
+}  // namespace
 
 // FileEnumerator::FileInfo ----------------------------------------------------
 
@@ -42,31 +59,36 @@
 FileEnumerator::FileEnumerator(const FilePath& root_path,
                                bool recursive,
                                int file_type)
-    : current_directory_entry_(0),
-      root_path_(root_path),
-      recursive_(recursive),
-      file_type_(file_type) {
-  // INCLUDE_DOT_DOT must not be specified if recursive.
-  DCHECK(!(recursive && (INCLUDE_DOT_DOT & file_type_)));
-  pending_paths_.push(root_path);
-}
+    : FileEnumerator(root_path,
+                     recursive,
+                     file_type,
+                     FilePath::StringType(),
+                     FolderSearchPolicy::MATCH_ONLY) {}
 
 FileEnumerator::FileEnumerator(const FilePath& root_path,
                                bool recursive,
                                int file_type,
                                const FilePath::StringType& pattern)
+    : FileEnumerator(root_path,
+                     recursive,
+                     file_type,
+                     pattern,
+                     FolderSearchPolicy::MATCH_ONLY) {}
+
+FileEnumerator::FileEnumerator(const FilePath& root_path,
+                               bool recursive,
+                               int file_type,
+                               const FilePath::StringType& pattern,
+                               FolderSearchPolicy folder_search_policy)
     : current_directory_entry_(0),
       root_path_(root_path),
       recursive_(recursive),
       file_type_(file_type),
-      pattern_(root_path.Append(pattern).value()) {
+      pattern_(pattern),
+      folder_search_policy_(folder_search_policy) {
   // INCLUDE_DOT_DOT must not be specified if recursive.
   DCHECK(!(recursive && (INCLUDE_DOT_DOT & file_type_)));
-  // The Windows version of this code appends the pattern to the root_path,
-  // potentially only matching against items in the top-most directory.
-  // Do the same here.
-  if (pattern.empty())
-    pattern_ = FilePath::StringType();
+
   pending_paths_.push(root_path);
 }
 
@@ -74,6 +96,8 @@
 }
 
 FilePath FileEnumerator::Next() {
+  base::ThreadRestrictions::AssertIOAllowed();
+
   ++current_directory_entry_;
 
   // While we've exhausted the entries in the current directory, do the next
@@ -85,29 +109,66 @@
     root_path_ = root_path_.StripTrailingSeparators();
     pending_paths_.pop();
 
-    std::vector<FileInfo> entries;
-    if (!ReadDirectory(&entries, root_path_, file_type_ & SHOW_SYM_LINKS))
+    DIR* dir = opendir(root_path_.value().c_str());
+    if (!dir)
       continue;
 
     directory_entries_.clear();
+
+#if defined(OS_FUCHSIA)
+    // Fuchsia does not support .. on the file system server side, see
+    // https://fuchsia.googlesource.com/docs/+/master/dotdot.md and
+    // https://crbug.com/735540. However, for UI purposes, having the parent
+    // directory show up in directory listings makes sense, so we add it here to
+    // match the expectation on other operating systems. In cases where this
+    // is useful it should be resolvable locally.
+    FileInfo dotdot;
+    dotdot.stat_.st_mode = S_IFDIR;
+    dotdot.filename_ = FilePath("..");
+    directory_entries_->push_back(dotdot);
+#endif  // OS_FUCHSIA
+
     current_directory_entry_ = 0;
-    for (std::vector<FileInfo>::const_iterator i = entries.begin();
-         i != entries.end(); ++i) {
-      FilePath full_path = root_path_.Append(i->filename_);
-      if (ShouldSkip(full_path))
+    struct dirent* dent;
+    while ((dent = readdir(dir))) {
+      FileInfo info;
+      info.filename_ = FilePath(dent->d_name);
+
+      if (ShouldSkip(info.filename_))
         continue;
 
-      if (pattern_.size() &&
-          fnmatch(pattern_.c_str(), full_path.value().c_str(), FNM_NOESCAPE))
+      const bool is_pattern_matched = IsPatternMatched(info.filename_);
+
+      // MATCH_ONLY policy enumerates files and directories which matching
+      // pattern only. So we can early skip further checks.
+      if (folder_search_policy_ == FolderSearchPolicy::MATCH_ONLY &&
+          !is_pattern_matched)
         continue;
 
-      if (recursive_ && S_ISDIR(i->stat_.st_mode))
+      // Do not call OS stat/lstat if there is no sense to do it. If pattern is
+      // not matched (file will not appear in results) and search is not
+      // recursive (possible directory will not be added to pending paths) -
+      // there is no sense to obtain item below.
+      if (!recursive_ && !is_pattern_matched)
+        continue;
+
+      const FilePath full_path = root_path_.Append(info.filename_);
+      GetStat(full_path, file_type_ & SHOW_SYM_LINKS, &info.stat_);
+
+      const bool is_dir = S_ISDIR(info.stat_.st_mode);
+
+      if (recursive_ && is_dir)
         pending_paths_.push(full_path);
 
-      if ((S_ISDIR(i->stat_.st_mode) && (file_type_ & DIRECTORIES)) ||
-          (!S_ISDIR(i->stat_.st_mode) && (file_type_ & FILES)))
-        directory_entries_.push_back(*i);
+      if (is_pattern_matched && IsTypeMatched(is_dir))
+        directory_entries_.push_back(std::move(info));
     }
+    closedir(dir);
+
+    // MATCH_ONLY policy enumerates files in matched subfolders by "*" pattern.
+    // ALL policy enumerates files in all subfolders by origin pattern.
+    if (folder_search_policy_ == FolderSearchPolicy::MATCH_ONLY)
+      pattern_.clear();
   }
 
   return root_path_.Append(
@@ -118,51 +179,9 @@
   return directory_entries_[current_directory_entry_];
 }
 
-bool FileEnumerator::ReadDirectory(std::vector<FileInfo>* entries,
-                                   const FilePath& source, bool show_links) {
-  base::ThreadRestrictions::AssertIOAllowed();
-  DIR* dir = opendir(source.value().c_str());
-  if (!dir)
-    return false;
-
-#if defined(OS_FUCHSIA)
-  // Fuchsia does not support .. on the file system server side, see
-  // https://fuchsia.googlesource.com/docs/+/master/dotdot.md and
-  // https://crbug.com/735540. However, for UI purposes, having the parent
-  // directory show up in directory listings makes sense, so we add it here to
-  // match the expectation on other operating systems. In cases where this
-  // is useful it should be resolvable locally.
-  FileInfo dotdot;
-  dotdot.stat_.st_mode = S_IFDIR;
-  dotdot.filename_ = FilePath("..");
-  entries->push_back(dotdot);
-#endif  // OS_FUCHSIA
-
-  struct dirent* dent;
-  while ((dent = readdir(dir))) {
-    FileInfo info;
-    info.filename_ = FilePath(dent->d_name);
-
-    FilePath full_name = source.Append(dent->d_name);
-    int ret;
-    if (show_links)
-      ret = lstat(full_name.value().c_str(), &info.stat_);
-    else
-      ret = stat(full_name.value().c_str(), &info.stat_);
-    if (ret < 0) {
-      // Print the stat() error message unless it was ENOENT and we're
-      // following symlinks.
-      if (!(errno == ENOENT && !show_links)) {
-        DPLOG(ERROR) << "Couldn't stat "
-                     << source.Append(dent->d_name).value();
-      }
-      memset(&info.stat_, 0, sizeof(info.stat_));
-    }
-    entries->push_back(info);
-  }
-
-  closedir(dir);
-  return true;
+bool FileEnumerator::IsPatternMatched(const FilePath& path) const {
+  return pattern_.empty() ||
+         !fnmatch(pattern_.c_str(), path.value().c_str(), FNM_NOESCAPE);
 }
 
 }  // namespace base
diff --git a/base/files/file_enumerator_unittest.cc b/base/files/file_enumerator_unittest.cc
new file mode 100644
index 0000000..5faa80f
--- /dev/null
+++ b/base/files/file_enumerator_unittest.cc
@@ -0,0 +1,312 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/files/file_enumerator.h"
+
+#include <deque>
+#include <utility>
+#include <vector>
+
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::ElementsAre;
+using testing::IsEmpty;
+using testing::UnorderedElementsAre;
+
+namespace base {
+namespace {
+
+const FilePath::StringType kEmptyPattern;
+
+const std::vector<FileEnumerator::FolderSearchPolicy> kFolderSearchPolicies{
+    FileEnumerator::FolderSearchPolicy::MATCH_ONLY,
+    FileEnumerator::FolderSearchPolicy::ALL};
+
+std::deque<FilePath> RunEnumerator(
+    const FilePath& root_path,
+    bool recursive,
+    int file_type,
+    const FilePath::StringType& pattern,
+    FileEnumerator::FolderSearchPolicy folder_search_policy) {
+  std::deque<FilePath> rv;
+  FileEnumerator enumerator(root_path, recursive, file_type, pattern,
+                            folder_search_policy);
+  for (auto file = enumerator.Next(); !file.empty(); file = enumerator.Next())
+    rv.emplace_back(std::move(file));
+  return rv;
+}
+
+bool CreateDummyFile(const FilePath& path) {
+  return WriteFile(path, "42", sizeof("42")) == sizeof("42");
+}
+
+}  // namespace
+
+TEST(FileEnumerator, NotExistingPath) {
+  const FilePath path = FilePath::FromUTF8Unsafe("some_not_existing_path");
+  ASSERT_FALSE(PathExists(path));
+
+  for (auto policy : kFolderSearchPolicies) {
+    const auto files = RunEnumerator(
+        path, true, FileEnumerator::FILES & FileEnumerator::DIRECTORIES,
+        FILE_PATH_LITERAL(""), policy);
+    EXPECT_THAT(files, IsEmpty());
+  }
+}
+
+TEST(FileEnumerator, EmptyFolder) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  for (auto policy : kFolderSearchPolicies) {
+    const auto files =
+        RunEnumerator(temp_dir.GetPath(), true,
+                      FileEnumerator::FILES & FileEnumerator::DIRECTORIES,
+                      kEmptyPattern, policy);
+    EXPECT_THAT(files, IsEmpty());
+  }
+}
+
+TEST(FileEnumerator, SingleFileInFolderForFileSearch) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath& path = temp_dir.GetPath();
+  const FilePath file = path.AppendASCII("test.txt");
+  ASSERT_TRUE(CreateDummyFile(file));
+
+  for (auto policy : kFolderSearchPolicies) {
+    const auto files = RunEnumerator(
+        temp_dir.GetPath(), true, FileEnumerator::FILES, kEmptyPattern, policy);
+    EXPECT_THAT(files, ElementsAre(file));
+  }
+}
+
+TEST(FileEnumerator, SingleFileInFolderForDirSearch) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath& path = temp_dir.GetPath();
+  ASSERT_TRUE(CreateDummyFile(path.AppendASCII("test.txt")));
+
+  for (auto policy : kFolderSearchPolicies) {
+    const auto files = RunEnumerator(path, true, FileEnumerator::DIRECTORIES,
+                                     kEmptyPattern, policy);
+    EXPECT_THAT(files, IsEmpty());
+  }
+}
+
+TEST(FileEnumerator, SingleFileInFolderWithFiltering) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath& path = temp_dir.GetPath();
+  const FilePath file = path.AppendASCII("test.txt");
+  ASSERT_TRUE(CreateDummyFile(file));
+
+  for (auto policy : kFolderSearchPolicies) {
+    auto files = RunEnumerator(path, true, FileEnumerator::FILES,
+                               FILE_PATH_LITERAL("*.txt"), policy);
+    EXPECT_THAT(files, ElementsAre(file));
+
+    files = RunEnumerator(path, true, FileEnumerator::FILES,
+                          FILE_PATH_LITERAL("*.pdf"), policy);
+    EXPECT_THAT(files, IsEmpty());
+  }
+}
+
+TEST(FileEnumerator, TwoFilesInFolder) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath& path = temp_dir.GetPath();
+  const FilePath foo_txt = path.AppendASCII("foo.txt");
+  const FilePath bar_txt = path.AppendASCII("bar.txt");
+  ASSERT_TRUE(CreateDummyFile(foo_txt));
+  ASSERT_TRUE(CreateDummyFile(bar_txt));
+
+  for (auto policy : kFolderSearchPolicies) {
+    auto files = RunEnumerator(path, true, FileEnumerator::FILES,
+                               FILE_PATH_LITERAL("*.txt"), policy);
+    EXPECT_THAT(files, UnorderedElementsAre(foo_txt, bar_txt));
+
+    files = RunEnumerator(path, true, FileEnumerator::FILES,
+                          FILE_PATH_LITERAL("foo*"), policy);
+    EXPECT_THAT(files, ElementsAre(foo_txt));
+
+    files = RunEnumerator(path, true, FileEnumerator::FILES,
+                          FILE_PATH_LITERAL("*.pdf"), policy);
+    EXPECT_THAT(files, IsEmpty());
+
+    files =
+        RunEnumerator(path, true, FileEnumerator::FILES, kEmptyPattern, policy);
+    EXPECT_THAT(files, UnorderedElementsAre(foo_txt, bar_txt));
+  }
+}
+
+TEST(FileEnumerator, SingleFolderInFolderForFileSearch) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath& path = temp_dir.GetPath();
+
+  ScopedTempDir temp_subdir;
+  ASSERT_TRUE(temp_subdir.CreateUniqueTempDirUnderPath(path));
+
+  for (auto policy : kFolderSearchPolicies) {
+    const auto files =
+        RunEnumerator(path, true, FileEnumerator::FILES, kEmptyPattern, policy);
+    EXPECT_THAT(files, IsEmpty());
+  }
+}
+
+TEST(FileEnumerator, SingleFolderInFolderForDirSearch) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath& path = temp_dir.GetPath();
+
+  ScopedTempDir temp_subdir;
+  ASSERT_TRUE(temp_subdir.CreateUniqueTempDirUnderPath(path));
+
+  for (auto policy : kFolderSearchPolicies) {
+    const auto files = RunEnumerator(path, true, FileEnumerator::DIRECTORIES,
+                                     kEmptyPattern, policy);
+    EXPECT_THAT(files, ElementsAre(temp_subdir.GetPath()));
+  }
+}
+
+TEST(FileEnumerator, TwoFoldersInFolder) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath& path = temp_dir.GetPath();
+
+  const FilePath subdir_foo = path.AppendASCII("foo");
+  const FilePath subdir_bar = path.AppendASCII("bar");
+  ASSERT_TRUE(CreateDirectory(subdir_foo));
+  ASSERT_TRUE(CreateDirectory(subdir_bar));
+
+  for (auto policy : kFolderSearchPolicies) {
+    auto files = RunEnumerator(path, true, FileEnumerator::DIRECTORIES,
+                               kEmptyPattern, policy);
+    EXPECT_THAT(files, UnorderedElementsAre(subdir_foo, subdir_bar));
+
+    files = RunEnumerator(path, true, FileEnumerator::DIRECTORIES,
+                          FILE_PATH_LITERAL("foo"), policy);
+    EXPECT_THAT(files, ElementsAre(subdir_foo));
+  }
+}
+
+TEST(FileEnumerator, FolderAndFileInFolder) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath& path = temp_dir.GetPath();
+
+  ScopedTempDir temp_subdir;
+  ASSERT_TRUE(temp_subdir.CreateUniqueTempDirUnderPath(path));
+  const FilePath file = path.AppendASCII("test.txt");
+  ASSERT_TRUE(CreateDummyFile(file));
+
+  for (auto policy : kFolderSearchPolicies) {
+    auto files =
+        RunEnumerator(path, true, FileEnumerator::FILES, kEmptyPattern, policy);
+    EXPECT_THAT(files, ElementsAre(file));
+
+    files = RunEnumerator(path, true, FileEnumerator::DIRECTORIES,
+                          kEmptyPattern, policy);
+    EXPECT_THAT(files, ElementsAre(temp_subdir.GetPath()));
+
+    files = RunEnumerator(path, true,
+                          FileEnumerator::FILES | FileEnumerator::DIRECTORIES,
+                          kEmptyPattern, policy);
+    EXPECT_THAT(files, UnorderedElementsAre(file, temp_subdir.GetPath()));
+  }
+}
+
+TEST(FileEnumerator, FilesInParentFolderAlwaysFirst) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath& path = temp_dir.GetPath();
+
+  ScopedTempDir temp_subdir;
+  ASSERT_TRUE(temp_subdir.CreateUniqueTempDirUnderPath(path));
+  const FilePath foo_txt = path.AppendASCII("foo.txt");
+  const FilePath bar_txt = temp_subdir.GetPath().AppendASCII("bar.txt");
+  ASSERT_TRUE(CreateDummyFile(foo_txt));
+  ASSERT_TRUE(CreateDummyFile(bar_txt));
+
+  for (auto policy : kFolderSearchPolicies) {
+    const auto files =
+        RunEnumerator(path, true, FileEnumerator::FILES, kEmptyPattern, policy);
+    EXPECT_THAT(files, ElementsAre(foo_txt, bar_txt));
+  }
+}
+
+TEST(FileEnumerator, FileInSubfolder) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath subdir = temp_dir.GetPath().AppendASCII("subdir");
+  ASSERT_TRUE(CreateDirectory(subdir));
+
+  const FilePath file = subdir.AppendASCII("test.txt");
+  ASSERT_TRUE(CreateDummyFile(file));
+
+  for (auto policy : kFolderSearchPolicies) {
+    auto files = RunEnumerator(temp_dir.GetPath(), true, FileEnumerator::FILES,
+                               kEmptyPattern, policy);
+    EXPECT_THAT(files, ElementsAre(file));
+
+    files = RunEnumerator(temp_dir.GetPath(), false, FileEnumerator::FILES,
+                          kEmptyPattern, policy);
+    EXPECT_THAT(files, IsEmpty());
+  }
+}
+
+TEST(FileEnumerator, FilesInSubfoldersWithFiltering) {
+  ScopedTempDir temp_dir;
+  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+  const FilePath test_txt = temp_dir.GetPath().AppendASCII("test.txt");
+  const FilePath subdir_foo = temp_dir.GetPath().AppendASCII("foo_subdir");
+  const FilePath subdir_bar = temp_dir.GetPath().AppendASCII("bar_subdir");
+  const FilePath foo_test = subdir_foo.AppendASCII("test.txt");
+  const FilePath foo_foo = subdir_foo.AppendASCII("foo.txt");
+  const FilePath foo_bar = subdir_foo.AppendASCII("bar.txt");
+  const FilePath bar_test = subdir_bar.AppendASCII("test.txt");
+  const FilePath bar_foo = subdir_bar.AppendASCII("foo.txt");
+  const FilePath bar_bar = subdir_bar.AppendASCII("bar.txt");
+  ASSERT_TRUE(CreateDummyFile(test_txt));
+  ASSERT_TRUE(CreateDirectory(subdir_foo));
+  ASSERT_TRUE(CreateDirectory(subdir_bar));
+  ASSERT_TRUE(CreateDummyFile(foo_test));
+  ASSERT_TRUE(CreateDummyFile(foo_foo));
+  ASSERT_TRUE(CreateDummyFile(foo_bar));
+  ASSERT_TRUE(CreateDummyFile(bar_test));
+  ASSERT_TRUE(CreateDummyFile(bar_foo));
+  ASSERT_TRUE(CreateDummyFile(bar_bar));
+
+  auto files =
+      RunEnumerator(temp_dir.GetPath(), true,
+                    FileEnumerator::FILES | FileEnumerator::DIRECTORIES,
+                    FILE_PATH_LITERAL("foo*"),
+                    FileEnumerator::FolderSearchPolicy::MATCH_ONLY);
+  EXPECT_THAT(files,
+              UnorderedElementsAre(subdir_foo, foo_test, foo_foo, foo_bar));
+
+  files = RunEnumerator(temp_dir.GetPath(), true,
+                        FileEnumerator::FILES | FileEnumerator::DIRECTORIES,
+                        FILE_PATH_LITERAL("foo*"),
+                        FileEnumerator::FolderSearchPolicy::ALL);
+  EXPECT_THAT(files, UnorderedElementsAre(subdir_foo, foo_foo, bar_foo));
+}
+
+}  // namespace base