/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * 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.
 */

#include "src/traced/probes/filesystem/file_scanner.h"

#include <sys/stat.h>
#include <memory>
#include <string>

#include "perfetto/base/logging.h"
#include "protos/perfetto/trace/filesystem/inode_file_map.pbzero.h"
#include "src/base/test/test_task_runner.h"
#include "src/base/test/utils.h"
#include "test/gtest_and_gmock.h"

namespace perfetto {
namespace {

using ::testing::Contains;
using ::testing::Eq;
using ::testing::UnorderedElementsAre;

class TestDelegate : public FileScanner::Delegate {
 public:
  TestDelegate(std::function<bool(BlockDeviceID,
                                  Inode,
                                  const std::string&,
                                  InodeFileMap_Entry_Type)> callback,
               std::function<void()> done_callback)
      : callback_(std::move(callback)),
        done_callback_(std::move(done_callback)) {}
  bool OnInodeFound(BlockDeviceID block_device_id,
                    Inode inode,
                    const std::string& path,
                    InodeFileMap_Entry_Type type) override {
    return callback_(block_device_id, inode, path, type);
  }

  void OnInodeScanDone() { return done_callback_(); }

 private:
  std::function<
      bool(BlockDeviceID, Inode, const std::string&, InodeFileMap_Entry_Type)>
      callback_;
  std::function<void()> done_callback_;
};

struct FileEntry {
  FileEntry(BlockDeviceID block_device_id,
            Inode inode,
            std::string path,
            InodeFileMap_Entry_Type type)
      : block_device_id_(block_device_id),
        inode_(inode),
        path_(std::move(path)),
        type_(type) {}

  bool operator==(const FileEntry& other) const {
    return block_device_id_ == other.block_device_id_ &&
           inode_ == other.inode_ && path_ == other.path_ &&
           type_ == other.type_;
  }

  BlockDeviceID block_device_id_;
  Inode inode_;
  std::string path_;
  InodeFileMap_Entry_Type type_;
};

struct stat CheckStat(const std::string& path) {
  struct stat buf{};
  PERFETTO_CHECK(lstat(path.c_str(), &buf) != -1);
  return buf;
}

FileEntry StatFileEntry(const std::string& path, InodeFileMap_Entry_Type type) {
  struct stat buf = CheckStat(path);
  return FileEntry(buf.st_dev, buf.st_ino, path, type);
}

TEST(FileScannerTest, TestSynchronousStop) {
  uint64_t seen = 0;
  bool done = false;
  TestDelegate delegate(
      [&seen](BlockDeviceID, Inode, const std::string&,
              InodeFileMap_Entry_Type) {
        ++seen;
        return false;
      },
      [&done] { done = true; });

  FileScanner fs(
      {base::GetTestDataPath("src/traced/probes/filesystem/testdata")},
      &delegate);
  fs.Scan();

  EXPECT_EQ(seen, 1u);
  EXPECT_TRUE(done);
}

TEST(FileScannerTest, TestAsynchronousStop) {
  uint64_t seen = 0;
  base::TestTaskRunner task_runner;
  TestDelegate delegate(
      [&seen](BlockDeviceID, Inode, const std::string&,
              InodeFileMap_Entry_Type) {
        ++seen;
        return false;
      },
      task_runner.CreateCheckpoint("done"));

  FileScanner fs(
      {base::GetTestDataPath("src/traced/probes/filesystem/testdata")},
      &delegate, 1, 1);
  fs.Scan(&task_runner);

  task_runner.RunUntilCheckpoint("done");

  EXPECT_EQ(seen, 1u);
}

TEST(FileScannerTest, TestSynchronousFindFiles) {
  std::vector<FileEntry> file_entries;
  TestDelegate delegate(
      [&file_entries](BlockDeviceID block_device_id, Inode inode,
                      const std::string& path, InodeFileMap_Entry_Type type) {
        file_entries.emplace_back(block_device_id, inode, path, type);
        return true;
      },
      [] {});

  FileScanner fs(
      {base::GetTestDataPath("src/traced/probes/filesystem/testdata")},
      &delegate);
  fs.Scan();

  EXPECT_THAT(
      file_entries,
      UnorderedElementsAre(
          Eq(StatFileEntry(
              base::GetTestDataPath(
                  "src/traced/probes/filesystem/testdata/dir1/file1"),
              protos::pbzero::InodeFileMap::Entry::Type::FILE)),
          Eq(StatFileEntry(base::GetTestDataPath(
                               "src/traced/probes/filesystem/testdata/file2"),
                           protos::pbzero::InodeFileMap::Entry::Type::FILE)),
          Eq(StatFileEntry(
              base::GetTestDataPath(
                  "src/traced/probes/filesystem/testdata/dir1"),
              protos::pbzero::InodeFileMap::Entry::Type::DIRECTORY))));
}

TEST(FileScannerTest, TestAsynchronousFindFiles) {
  base::TestTaskRunner task_runner;
  std::vector<FileEntry> file_entries;
  TestDelegate delegate(
      [&file_entries](BlockDeviceID block_device_id, Inode inode,
                      const std::string& path, InodeFileMap_Entry_Type type) {
        file_entries.emplace_back(block_device_id, inode, path, type);
        return true;
      },
      task_runner.CreateCheckpoint("done"));

  FileScanner fs(
      {base::GetTestDataPath("src/traced/probes/filesystem/testdata")},
      &delegate, 1, 1);
  fs.Scan(&task_runner);

  task_runner.RunUntilCheckpoint("done");

  EXPECT_THAT(
      file_entries,
      UnorderedElementsAre(
          Eq(StatFileEntry(
              base::GetTestDataPath(
                  "src/traced/probes/filesystem/testdata/dir1/file1"),
              protos::pbzero::InodeFileMap::Entry::Type::FILE)),
          Eq(StatFileEntry(base::GetTestDataPath(
                               "src/traced/probes/filesystem/testdata/file2"),
                           protos::pbzero::InodeFileMap::Entry::Type::FILE)),
          Eq(StatFileEntry(
              base::GetTestDataPath(
                  "src/traced/probes/filesystem/testdata/dir1"),
              protos::pbzero::InodeFileMap::Entry::Type::DIRECTORY))));
}

}  // namespace
}  // namespace perfetto
