// // Copyright 2019 The Abseil Authors. // // 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 // // https://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 "absl/flags/parse.h" #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/base/internal/raw_logging.h" #include "absl/base/internal/scoped_set_env.h" #include "absl/flags/flag.h" #include "absl/strings/str_cat.h" #include "absl/strings/substitute.h" #include "absl/types/span.h" #ifdef _WIN32 #include #endif namespace { using absl::base_internal::ScopedSetEnv; struct UDT { UDT() = default; UDT(const UDT&) = default; UDT(int v) : value(v) {} // NOLINT int value; }; bool AbslParseFlag(absl::string_view in, UDT* udt, std::string* err) { if (in == "A") { udt->value = 1; return true; } if (in == "AAA") { udt->value = 10; return true; } *err = "Use values A, AAA instead"; return false; } std::string AbslUnparseFlag(const UDT& udt) { return udt.value == 1 ? "A" : "AAA"; } std::string GetTestTmpDirEnvVar(const char* const env_var_name) { #ifdef _WIN32 char buf[MAX_PATH]; auto get_res = GetEnvironmentVariableA(env_var_name, buf, sizeof(buf)); if (get_res >= sizeof(buf) || get_res == 0) { return ""; } return std::string(buf, get_res); #else const char* val = ::getenv(env_var_name); if (val == nullptr) { return ""; } return val; #endif } const std::string& GetTestTempDir() { static std::string* temp_dir_name = []() -> std::string* { std::string* res = new std::string(GetTestTmpDirEnvVar("TEST_TMPDIR")); if (res->empty()) { *res = GetTestTmpDirEnvVar("TMPDIR"); } if (res->empty()) { #ifdef _WIN32 char temp_path_buffer[MAX_PATH]; auto len = GetTempPathA(MAX_PATH, temp_path_buffer); if (len < MAX_PATH && len != 0) { std::string temp_dir_name = absl::StrCat( temp_path_buffer, "\\parse_test.", GetCurrentProcessId()); if (CreateDirectoryA(temp_dir_name.c_str(), nullptr)) { *res = temp_dir_name; } } #else char temp_dir_template[] = "/tmp/parse_test.XXXXXX"; if (auto* unique_name = ::mkdtemp(temp_dir_template)) { *res = unique_name; } #endif if (res->empty()) { ABSL_INTERNAL_LOG(FATAL, "Failed to make temporary directory for data files"); } } #ifdef _WIN32 *res += "\\"; #else *res += "/"; #endif return res; }(); return *temp_dir_name; } struct FlagfileData { const absl::string_view file_name; const absl::Span file_lines; }; // clang-format off constexpr const char* const ff1_data[] = { "# comment ", " # comment ", "", " ", "--int_flag=-1", " --string_flag=q2w2 ", " ## ", " --double_flag=0.1", "--bool_flag=Y " }; constexpr const char* const ff2_data[] = { "# Setting legacy flag", "--legacy_int=1111", "--legacy_bool", "--nobool_flag", "--legacy_str=aqsw", "--int_flag=100", " ## =============" }; // clang-format on // Builds flagfile flag in the flagfile_flag buffer and returns it. This // function also creates a temporary flagfile based on FlagfileData input. // We create a flagfile in a temporary directory with the name specified in // FlagfileData and populate it with lines specifed in FlagfileData. If $0 is // referenced in any of the lines in FlagfileData they are replaced with // temporary directory location. This way we can test inclusion of one flagfile // from another flagfile. const char* GetFlagfileFlag(const std::vector& ffd, std::string* flagfile_flag) { *flagfile_flag = "--flagfile="; absl::string_view separator; for (const auto& flagfile_data : ffd) { std::string flagfile_name = absl::StrCat(GetTestTempDir(), flagfile_data.file_name); std::ofstream flagfile_out(flagfile_name); for (auto line : flagfile_data.file_lines) { flagfile_out << absl::Substitute(line, GetTestTempDir()) << "\n"; } absl::StrAppend(flagfile_flag, separator, flagfile_name); separator = ","; } return flagfile_flag->c_str(); } } // namespace ABSL_FLAG(int, int_flag, 1, ""); ABSL_FLAG(double, double_flag, 1.1, ""); ABSL_FLAG(std::string, string_flag, "a", ""); ABSL_FLAG(bool, bool_flag, false, ""); ABSL_FLAG(UDT, udt_flag, -1, ""); ABSL_RETIRED_FLAG(int, legacy_int, 1, ""); ABSL_RETIRED_FLAG(bool, legacy_bool, false, ""); ABSL_RETIRED_FLAG(std::string, legacy_str, "l", ""); namespace { namespace flags = absl::flags_internal; using testing::ElementsAreArray; class ParseTest : public testing::Test { private: flags::FlagSaver flag_saver_; }; // -------------------------------------------------------------------- template std::vector InvokeParse(const char* (&in_argv)[N]) { return absl::ParseCommandLine(N, const_cast(in_argv)); } // -------------------------------------------------------------------- template void TestParse(const char* (&in_argv)[N], int int_flag_value, double double_flag_val, absl::string_view string_flag_val, bool bool_flag_val, int exp_position_args = 0) { auto out_args = InvokeParse(in_argv); EXPECT_EQ(out_args.size(), 1 + exp_position_args); EXPECT_STREQ(out_args[0], "testbin"); EXPECT_EQ(absl::GetFlag(FLAGS_int_flag), int_flag_value); EXPECT_NEAR(absl::GetFlag(FLAGS_double_flag), double_flag_val, 0.0001); EXPECT_EQ(absl::GetFlag(FLAGS_string_flag), string_flag_val); EXPECT_EQ(absl::GetFlag(FLAGS_bool_flag), bool_flag_val); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestEmptyArgv) { const char* in_argv[] = {"testbin"}; auto out_args = InvokeParse(in_argv); EXPECT_EQ(out_args.size(), 1); EXPECT_STREQ(out_args[0], "testbin"); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestValidIntArg) { const char* in_args1[] = { "testbin", "--int_flag=10", }; TestParse(in_args1, 10, 1.1, "a", false); const char* in_args2[] = { "testbin", "-int_flag=020", }; TestParse(in_args2, 20, 1.1, "a", false); const char* in_args3[] = { "testbin", "--int_flag", "-30", }; TestParse(in_args3, -30, 1.1, "a", false); const char* in_args4[] = { "testbin", "-int_flag", "0x21", }; TestParse(in_args4, 33, 1.1, "a", false); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestValidDoubleArg) { const char* in_args1[] = { "testbin", "--double_flag=2.3", }; TestParse(in_args1, 1, 2.3, "a", false); const char* in_args2[] = { "testbin", "--double_flag=0x1.2", }; TestParse(in_args2, 1, 1.125, "a", false); const char* in_args3[] = { "testbin", "--double_flag", "99.7", }; TestParse(in_args3, 1, 99.7, "a", false); const char* in_args4[] = { "testbin", "--double_flag", "0x20.1", }; TestParse(in_args4, 1, 32.0625, "a", false); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestValidStringArg) { const char* in_args1[] = { "testbin", "--string_flag=aqswde", }; TestParse(in_args1, 1, 1.1, "aqswde", false); const char* in_args2[] = { "testbin", "-string_flag=a=b=c", }; TestParse(in_args2, 1, 1.1, "a=b=c", false); const char* in_args3[] = { "testbin", "--string_flag", "zaxscd", }; TestParse(in_args3, 1, 1.1, "zaxscd", false); const char* in_args4[] = { "testbin", "-string_flag", "--int_flag", }; TestParse(in_args4, 1, 1.1, "--int_flag", false); const char* in_args5[] = { "testbin", "--string_flag", "--no_a_flag=11", }; TestParse(in_args5, 1, 1.1, "--no_a_flag=11", false); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestValidBoolArg) { const char* in_args1[] = { "testbin", "--bool_flag", }; TestParse(in_args1, 1, 1.1, "a", true); const char* in_args2[] = { "testbin", "--nobool_flag", }; TestParse(in_args2, 1, 1.1, "a", false); const char* in_args3[] = { "testbin", "--bool_flag=true", }; TestParse(in_args3, 1, 1.1, "a", true); const char* in_args4[] = { "testbin", "-bool_flag=false", }; TestParse(in_args4, 1, 1.1, "a", false); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestValidUDTArg) { const char* in_args1[] = { "testbin", "--udt_flag=A", }; InvokeParse(in_args1); EXPECT_EQ(absl::GetFlag(FLAGS_udt_flag).value, 1); const char* in_args2[] = {"testbin", "--udt_flag", "AAA"}; InvokeParse(in_args2); EXPECT_EQ(absl::GetFlag(FLAGS_udt_flag).value, 10); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestValidMultipleArg) { const char* in_args1[] = { "testbin", "--bool_flag", "--int_flag=2", "--double_flag=0.1", "--string_flag=asd", }; TestParse(in_args1, 2, 0.1, "asd", true); const char* in_args2[] = { "testbin", "--string_flag=", "--nobool_flag", "--int_flag", "-011", "--double_flag", "-1e-2", }; TestParse(in_args2, -11, -0.01, "", false); const char* in_args3[] = { "testbin", "--int_flag", "-0", "--string_flag", "\"\"", "--bool_flag=true", "--double_flag=1e18", }; TestParse(in_args3, 0, 1e18, "\"\"", true); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestPositionalArgs) { const char* in_args1[] = { "testbin", "p1", "p2", }; TestParse(in_args1, 1, 1.1, "a", false, 2); auto out_args1 = InvokeParse(in_args1); EXPECT_STREQ(out_args1[1], "p1"); EXPECT_STREQ(out_args1[2], "p2"); const char* in_args2[] = { "testbin", "--int_flag=2", "p1", }; TestParse(in_args2, 2, 1.1, "a", false, 1); auto out_args2 = InvokeParse(in_args2); EXPECT_STREQ(out_args2[1], "p1"); const char* in_args3[] = {"testbin", "p1", "--int_flag=3", "p2", "--bool_flag", "true"}; TestParse(in_args3, 3, 1.1, "a", true, 3); auto out_args3 = InvokeParse(in_args3); EXPECT_STREQ(out_args3[1], "p1"); EXPECT_STREQ(out_args3[2], "p2"); EXPECT_STREQ(out_args3[3], "true"); const char* in_args4[] = { "testbin", "--", "p1", "p2", }; TestParse(in_args4, 3, 1.1, "a", true, 2); auto out_args4 = InvokeParse(in_args4); EXPECT_STREQ(out_args4[1], "p1"); EXPECT_STREQ(out_args4[2], "p2"); const char* in_args5[] = { "testbin", "p1", "--int_flag=4", "--", "--bool_flag", "false", "p2", }; TestParse(in_args5, 4, 1.1, "a", true, 4); auto out_args5 = InvokeParse(in_args5); EXPECT_STREQ(out_args5[1], "p1"); EXPECT_STREQ(out_args5[2], "--bool_flag"); EXPECT_STREQ(out_args5[3], "false"); EXPECT_STREQ(out_args5[4], "p2"); } // -------------------------------------------------------------------- using ParseDeathTest = ParseTest; TEST_F(ParseDeathTest, TestUndefinedArg) { const char* in_args1[] = { "testbin", "--undefined_flag", }; EXPECT_DEATH(InvokeParse(in_args1), "Unknown command line flag 'undefined_flag'"); const char* in_args2[] = { "testbin", "--noprefixed_flag", }; EXPECT_DEATH(InvokeParse(in_args2), "Unknown command line flag 'noprefixed_flag'"); const char* in_args3[] = { "testbin", "--Int_flag=1", }; EXPECT_DEATH(InvokeParse(in_args3), "Unknown command line flag 'Int_flag'"); } // -------------------------------------------------------------------- TEST_F(ParseDeathTest, TestInvalidBoolFlagFormat) { const char* in_args1[] = { "testbin", "--bool_flag=", }; EXPECT_DEATH( InvokeParse(in_args1), "Missing the value after assignment for the boolean flag 'bool_flag'"); const char* in_args2[] = { "testbin", "--nobool_flag=true", }; EXPECT_DEATH(InvokeParse(in_args2), "Negative form with assignment is not valid for the boolean " "flag 'bool_flag'"); } // -------------------------------------------------------------------- TEST_F(ParseDeathTest, TestInvalidNonBoolFlagFormat) { const char* in_args1[] = { "testbin", "--nostring_flag", }; EXPECT_DEATH(InvokeParse(in_args1), "Negative form is not valid for the flag 'string_flag'"); const char* in_args2[] = { "testbin", "--int_flag", }; EXPECT_DEATH(InvokeParse(in_args2), "Missing the value for the flag 'int_flag'"); } // -------------------------------------------------------------------- TEST_F(ParseDeathTest, TestInvalidUDTFlagFormat) { const char* in_args1[] = { "testbin", "--udt_flag=1", }; EXPECT_DEATH(InvokeParse(in_args1), "Illegal value '1' specified for flag 'udt_flag'; Use values A, " "AAA instead"); const char* in_args2[] = { "testbin", "--udt_flag", "AA", }; EXPECT_DEATH(InvokeParse(in_args2), "Illegal value 'AA' specified for flag 'udt_flag'; Use values " "A, AAA instead"); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestLegacyFlags) { const char* in_args1[] = { "testbin", "--legacy_int=11", }; TestParse(in_args1, 1, 1.1, "a", false); const char* in_args2[] = { "testbin", "--legacy_bool", }; TestParse(in_args2, 1, 1.1, "a", false); const char* in_args3[] = { "testbin", "--legacy_int", "22", "--int_flag=2", "--legacy_bool", "true", "--legacy_str", "--string_flag=qwe", }; TestParse(in_args3, 2, 1.1, "a", false, 1); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestSimpleValidFlagfile) { std::string flagfile_flag; const char* in_args1[] = { "testbin", GetFlagfileFlag({{"parse_test.ff1", absl::MakeConstSpan(ff1_data)}}, &flagfile_flag), }; TestParse(in_args1, -1, 0.1, "q2w2 ", true); const char* in_args2[] = { "testbin", GetFlagfileFlag({{"parse_test.ff2", absl::MakeConstSpan(ff2_data)}}, &flagfile_flag), }; TestParse(in_args2, 100, 0.1, "q2w2 ", false); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestValidMultiFlagfile) { std::string flagfile_flag; const char* in_args1[] = { "testbin", GetFlagfileFlag({{"parse_test.ff2", absl::MakeConstSpan(ff2_data)}, {"parse_test.ff1", absl::MakeConstSpan(ff1_data)}}, &flagfile_flag), }; TestParse(in_args1, -1, 0.1, "q2w2 ", true); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestFlagfileMixedWithRegularFlags) { std::string flagfile_flag; const char* in_args1[] = { "testbin", "--int_flag=3", GetFlagfileFlag({{"parse_test.ff1", absl::MakeConstSpan(ff1_data)}}, &flagfile_flag), "-double_flag=0.2"}; TestParse(in_args1, -1, 0.2, "q2w2 ", true); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestFlagfileInFlagfile) { std::string flagfile_flag; constexpr const char* const ff3_data[] = { "--flagfile=$0/parse_test.ff1", "--flagfile=$0/parse_test.ff2", }; const char* in_args1[] = { "testbin", GetFlagfileFlag({{"parse_test.ff3", absl::MakeConstSpan(ff3_data)}}, &flagfile_flag), }; TestParse(in_args1, 100, 0.1, "q2w2 ", false); } // -------------------------------------------------------------------- TEST_F(ParseDeathTest, TestInvalidFlagfiles) { std::string flagfile_flag; constexpr const char* const ff4_data[] = { "--unknown_flag=10" }; const char* in_args1[] = { "testbin", GetFlagfileFlag({{"parse_test.ff4", absl::MakeConstSpan(ff4_data)}}, &flagfile_flag), }; EXPECT_DEATH(InvokeParse(in_args1), "Unknown command line flag 'unknown_flag'"); constexpr const char* const ff5_data[] = { "--int_flag 10", }; const char* in_args2[] = { "testbin", GetFlagfileFlag({{"parse_test.ff5", absl::MakeConstSpan(ff5_data)}}, &flagfile_flag), }; EXPECT_DEATH(InvokeParse(in_args2), "Unknown command line flag 'int_flag 10'"); constexpr const char* const ff6_data[] = { "--int_flag=10", "--", "arg1", "arg2", "arg3", }; const char* in_args3[] = { "testbin", GetFlagfileFlag({{"parse_test.ff6", absl::MakeConstSpan(ff6_data)}}, &flagfile_flag), }; EXPECT_DEATH(InvokeParse(in_args3), "Flagfile can't contain position arguments or --"); const char* in_args4[] = { "testbin", "--flagfile=invalid_flag_file", }; EXPECT_DEATH(InvokeParse(in_args4), "Can't open flagfile invalid_flag_file"); constexpr const char* const ff7_data[] = { "--int_flag=10", "*bin*", "--str_flag=aqsw", }; const char* in_args5[] = { "testbin", GetFlagfileFlag({{"parse_test.ff7", absl::MakeConstSpan(ff7_data)}}, &flagfile_flag), }; EXPECT_DEATH(InvokeParse(in_args5), "Unexpected line in the flagfile .*: \\*bin\\*"); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestReadingRequiredFlagsFromEnv) { const char* in_args1[] = {"testbin", "--fromenv=int_flag,bool_flag,string_flag"}; ScopedSetEnv set_int_flag("FLAGS_int_flag", "33"); ScopedSetEnv set_bool_flag("FLAGS_bool_flag", "True"); ScopedSetEnv set_string_flag("FLAGS_string_flag", "AQ12"); TestParse(in_args1, 33, 1.1, "AQ12", true); } // -------------------------------------------------------------------- TEST_F(ParseDeathTest, TestReadingUnsetRequiredFlagsFromEnv) { const char* in_args1[] = {"testbin", "--fromenv=int_flag"}; EXPECT_DEATH(InvokeParse(in_args1), "FLAGS_int_flag not found in environment"); } // -------------------------------------------------------------------- TEST_F(ParseDeathTest, TestRecursiveFlagsFromEnv) { const char* in_args1[] = {"testbin", "--fromenv=tryfromenv"}; ScopedSetEnv set_tryfromenv("FLAGS_tryfromenv", "int_flag"); EXPECT_DEATH(InvokeParse(in_args1), "Infinite recursion on flag tryfromenv"); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestReadingOptionalFlagsFromEnv) { const char* in_args1[] = { "testbin", "--tryfromenv=int_flag,bool_flag,string_flag,other_flag"}; ScopedSetEnv set_int_flag("FLAGS_int_flag", "17"); ScopedSetEnv set_bool_flag("FLAGS_bool_flag", "Y"); TestParse(in_args1, 17, 1.1, "a", true); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestReadingFlagsFromEnvMoxedWithRegularFlags) { const char* in_args1[] = { "testbin", "--bool_flag=T", "--tryfromenv=int_flag,bool_flag", "--int_flag=-21", }; ScopedSetEnv set_int_flag("FLAGS_int_flag", "-15"); ScopedSetEnv set_bool_flag("FLAGS_bool_flag", "F"); TestParse(in_args1, -21, 1.1, "a", false); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestKeepParsedArgs) { const char* in_args1[] = { "testbin", "arg1", "--bool_flag", "--int_flag=211", "arg2", "--double_flag=1.1", "--string_flag", "asd", "--", "arg3", "arg4", }; auto out_args1 = InvokeParse(in_args1); EXPECT_THAT( out_args1, ElementsAreArray({absl::string_view("testbin"), absl::string_view("arg1"), absl::string_view("arg2"), absl::string_view("arg3"), absl::string_view("arg4")})); auto out_args2 = flags::ParseCommandLineImpl( 11, const_cast(in_args1), flags::ArgvListAction::kKeepParsedArgs, flags::UsageFlagsAction::kHandleUsage, flags::OnUndefinedFlag::kAbortIfUndefined); EXPECT_THAT( out_args2, ElementsAreArray({absl::string_view("testbin"), absl::string_view("--bool_flag"), absl::string_view("--int_flag=211"), absl::string_view("--double_flag=1.1"), absl::string_view("--string_flag"), absl::string_view("asd"), absl::string_view("--"), absl::string_view("arg1"), absl::string_view("arg2"), absl::string_view("arg3"), absl::string_view("arg4")})); } // -------------------------------------------------------------------- TEST_F(ParseTest, TestIgnoreUndefinedFlags) { const char* in_args1[] = { "testbin", "arg1", "--undef_flag=aa", "--int_flag=21", }; auto out_args1 = flags::ParseCommandLineImpl( 4, const_cast(in_args1), flags::ArgvListAction::kRemoveParsedArgs, flags::UsageFlagsAction::kHandleUsage, flags::OnUndefinedFlag::kIgnoreUndefined); EXPECT_THAT(out_args1, ElementsAreArray({absl::string_view("testbin"), absl::string_view("arg1")})); EXPECT_EQ(absl::GetFlag(FLAGS_int_flag), 21); const char* in_args2[] = { "testbin", "arg1", "--undef_flag=aa", "--string_flag=AA", }; auto out_args2 = flags::ParseCommandLineImpl( 4, const_cast(in_args2), flags::ArgvListAction::kKeepParsedArgs, flags::UsageFlagsAction::kHandleUsage, flags::OnUndefinedFlag::kIgnoreUndefined); EXPECT_THAT( out_args2, ElementsAreArray( {absl::string_view("testbin"), absl::string_view("--undef_flag=aa"), absl::string_view("--string_flag=AA"), absl::string_view("arg1")})); EXPECT_EQ(absl::GetFlag(FLAGS_string_flag), "AA"); } // -------------------------------------------------------------------- TEST_F(ParseDeathTest, TestHelpFlagHandling) { const char* in_args1[] = { "testbin", "--help", }; EXPECT_EXIT(InvokeParse(in_args1), testing::ExitedWithCode(1), ""); const char* in_args2[] = { "testbin", "--help", "--int_flag=3", }; auto out_args2 = flags::ParseCommandLineImpl( 3, const_cast(in_args2), flags::ArgvListAction::kRemoveParsedArgs, flags::UsageFlagsAction::kIgnoreUsage, flags::OnUndefinedFlag::kAbortIfUndefined); EXPECT_EQ(absl::GetFlag(FLAGS_int_flag), 3); } } // namespace