folly/folly/cli/test/NestedCommandLineAppTest.cpp

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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 <folly/cli/NestedCommandLineApp.h>

#include <folly/Subprocess.h>
#include <folly/experimental/io/FsUtil.h>
#include <folly/portability/GTest.h>

#include <cstdlib>
#include <glog/logging.h>

namespace folly {
namespace test {

namespace {

std::string getHelperPath() {
  const auto* envPath = getenv("FOLLY_NESTED_CMDLINE_HELPER");
  if (envPath) {
    return envPath;
  }

  const auto basename = "nested_command_line_app_test_helper";
  auto path = fs::executable_path();
  path.remove_filename() /= basename;
  if (!fs::exists(path)) {
    path = path.parent_path().parent_path() / basename / basename;
  }
  return path.string();
}

std::string callHelper(
    std::initializer_list<std::string> args,
    int expectedExitCode = 0,
    int stdoutFd = -1) {
  static std::string helperPath = getHelperPath();

  std::vector<std::string> allArgs;
  allArgs.reserve(args.size() + 1);
  allArgs.push_back(helperPath);
  allArgs.insert(allArgs.end(), args.begin(), args.end());

  Subprocess::Options options;
  if (stdoutFd != -1) {
    options.stdoutFd(stdoutFd);
  } else {
    options.pipeStdout();
  }
  options.pipeStderr();

  Subprocess proc(allArgs, options);
  auto p = proc.communicate();
  EXPECT_EQ(expectedExitCode, proc.wait().exitStatus()) << p.second;

  return p.first;
}

} // namespace

TEST(ProgramOptionsTest, Errors) {
  callHelper({}, 1);
  callHelper({"--wtf", "foo"}, 1);
  callHelper({"qux"}, 1);
  callHelper({"--global-foo", "x", "foo"}, 1);
}

TEST(ProgramOptionsTest, Help) {
  // Not actually checking help output, just verifying that help doesn't fail
  callHelper({"--version"});
  callHelper({"-h"});
  callHelper({"-h", "foo"});
  callHelper({"-h", "bar"});
  callHelper({"-h", "--", "bar"});
  callHelper({"--help"});
  callHelper({"--help", "foo"});
  callHelper({"--help", "bar"});
  callHelper({"--help", "--", "bar"});
  callHelper({"help"});
  callHelper({"help", "foo"});
  callHelper({"help", "bar"});

  // wrong command name
  callHelper({"-h", "thunk"}, 1);
  callHelper({"--help", "thunk"}, 1);
  callHelper({"help", "thunk"}, 1);

  // anything after -- is parsed as arguments
  callHelper({"--", "help", "bar"}, 1);
}

TEST(ProgramOptionsTest, DevFull) {
  folly::File full("/dev/full", O_RDWR);
  callHelper({"-h"}, 1, full.fd());
  callHelper({"--help"}, 1, full.fd());
}

TEST(ProgramOptionsTest, CutArguments) {
  // anything after -- is parsed as arguments, including more --
  EXPECT_EQ(
      "running foo\n"
      "foo global-foo 43\n"
      "foo local-foo 42\n"
      "foo conflict-global 42\n"
      "foo conflict 42\n"
      "foo arg b\n"
      "foo arg --\n"
      "foo arg --local-foo\n"
      "foo arg 44\n"
      "foo arg a\n",
      callHelper(
          {"foo",
           "--global-foo",
           "43",
           "--",
           "b",
           "--",
           "--local-foo",
           "44",
           "a"}));
}

TEST(ProgramOptionsTest, Success) {
  EXPECT_EQ(
      "running foo\n"
      "foo global-foo 42\n"
      "foo local-foo 42\n"
      "foo conflict-global 42\n"
      "foo conflict 42\n",
      callHelper({"foo"}));

  EXPECT_EQ(
      "running foo\n"
      "foo global-foo 43\n"
      "foo local-foo 44\n"
      "foo conflict-global 42\n"
      "foo conflict 42\n"
      "foo arg a\n"
      "foo arg b\n",
      callHelper({"--global-foo", "43", "foo", "--local-foo", "44", "a", "b"}));

  // Check that global flags can still be given after the command
  EXPECT_EQ(
      "running foo\n"
      "foo global-foo 43\n"
      "foo local-foo 44\n"
      "foo conflict-global 42\n"
      "foo conflict 42\n"
      "foo arg a\n"
      "foo arg b\n",
      callHelper({"foo", "--global-foo", "43", "--local-foo", "44", "a", "b"}));
}

TEST(ProgramOptionsTest, Aliases) {
  EXPECT_EQ(
      "running foo\n"
      "foo global-foo 43\n"
      "foo local-foo 44\n"
      "foo conflict-global 42\n"
      "foo conflict 42\n"
      "foo arg a\n"
      "foo arg b\n",
      callHelper({"--global-foo", "43", "bar", "--local-foo", "44", "a", "b"}));
}

TEST(ProgramOptionsTest, PositionalArgsSuccess) {
  EXPECT_EQ(
      "running qux\n"
      "qux global-foo 43\n"
      "qux conflict-global 42\n"
      "qux optional-arg 41\n"
      "qux fred 44\n"
      "qux thud 21\n",
      callHelper({"--global-foo", "43", "qux", "44", "21"}));

  // Test with the optional arg and reposition the global flag after
  // positional commands
  EXPECT_EQ(
      "running qux\n"
      "qux global-foo 43\n"
      "qux conflict-global 42\n"
      "qux optional-arg 82\n"
      "qux fred 44\n"
      "qux thud 21\n",
      callHelper(
          {"qux", "44", "21", "--global-foo", "43", "--optional-arg", "82"}));

  // Test specifying positional args via keyword
  EXPECT_EQ(
      "running qux\n"
      "qux global-foo 43\n"
      "qux conflict-global 42\n"
      "qux optional-arg 82\n"
      "qux fred 21\n"
      "qux thud 11\n",
      callHelper(
          {"qux",
           "--optional-arg",
           "82",
           "--thud",
           "11",
           "--fred",
           "21",
           "--global-foo",
           "43"}));

  // When a subcommand specifies positional args any extra positional
  // args should cause an error
  callHelper(
      {"qux", "44", "21", "--global-foo", "43", "--optional-arg", "82", "a"},
      1);
  callHelper({"--global-foo", "43", "qux", "44", "21", "a"}, 1);
  callHelper(
      {"--global-foo", "43", "qux", "--fred", "44", "--thud", "21", "10"}, 1);
}

TEST(ProgramOptionsTest, BuiltinCommand) {
  NestedCommandLineApp app;
  ASSERT_TRUE(app.isBuiltinCommand(NestedCommandLineApp::kHelpCommand.str()));
  ASSERT_TRUE(
      app.isBuiltinCommand(NestedCommandLineApp::kVersionCommand.str()));
  ASSERT_FALSE(app.isBuiltinCommand(
      NestedCommandLineApp::kHelpCommand.str() + "nonsense"));
}

TEST(ProgramOptionsTest, ParseCallbacks) {
  NestedCommandLineApp app;

  std::vector<std::string> callbacks;
  app.addCallback([&](auto&, auto&, auto&) { callbacks.emplace_back("1"); });
  app.addCallback([&](auto&, auto&, auto&) { callbacks.emplace_back("2"); });

  bool invoked = false;
  app.addCommand("test", "", "", "", [&](auto&, auto&) {
    ASSERT_EQ((std::vector<std::string>{"1", "2"}), callbacks);
    invoked = true;
  });

  std::vector<std::string> args{"test"};
  app.run(args);

  ASSERT_TRUE(invoked);
  ASSERT_EQ((std::vector<std::string>{"1", "2"}), callbacks);
}

TEST(ProgramOptionsTest, ConflictingFlags) {
  EXPECT_EQ(
      "running foo\n"
      "foo global-foo 42\n"
      "foo local-foo 42\n"
      "foo conflict-global 43\n"
      "foo conflict 42\n",
      callHelper({"--conflict-global", "43", "foo"}));

  EXPECT_EQ(
      "running foo\n"
      "foo global-foo 42\n"
      "foo local-foo 42\n"
      "foo conflict-global 43\n"
      "foo conflict 42\n",
      callHelper({"foo", "--conflict-global", "43"}));

  EXPECT_EQ(
      "running foo\n"
      "foo global-foo 42\n"
      "foo local-foo 42\n"
      "foo conflict-global 42\n"
      "foo conflict 43\n",
      callHelper({"foo", "--conflict", "43"}));

  callHelper({"--conflict", "43", "foo"}, 1);
}

} // namespace test
} // namespace folly