diff --git a/doc/design/command-line-parser.md b/doc/design/command-line-parser.md new file mode 100644 index 0000000000..0531cea58a --- /dev/null +++ b/doc/design/command-line-parser.md @@ -0,0 +1,162 @@ +# Command Line Parser + +## Summary + +For command line parsing the POSIX `getopt` and `getopt_long` are available. +Those functions have several downsides: + + * Are not available on all platforms (Windows). + * Are error prone to use. The user has to consider all conversion edge cases + and verify the syntax. + * Are hard to use and the API is hard to read. + * One has to write a lot of code to use them, even for minimal problems. + * How the help is shown, how the help is formatted and certain syntax decision + can vary from binary to binary in iceoryx. A unified appearance and usage + across all applications would increase the user experience. + +Since we would like to offer command line support on all platforms we either +have to rewrite `getopt` and `getopt_long` from scratch or write a modern C++ +alternative. + +## Terminology + +The terminology is close to the terminology used in the POSIX.1-2017 standard. +For more details see +[Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). + +Assume we have a iceoryx tool with command line arguments like: +``` +iox_tool -a --bla -c 123 --dr_dotter blubb +``` + +| Name | Description | +| :---------------- | :------------------------------------------------------- | +| argument | A chain of arbitrary symbols after the command. Arguments are separated by space. Examples: `-a`, `--bla` | +| options | Arguments which start with a single dash `-` or double dash `--`, like `-a` or `--bla`. | +| short option | Argument which starts with a single dash followed by a non-dash symbol, like `-a` or `-c` | +| long option | Argument which starts with double dash followed by at least another non-dash symbol, like `--bla` or `--dr_dotter` | +| option name | The symbols after the leading dash of an option, for instance `a` or `dr_dotter`. | +| option argument | Argument which is separated by a space from an option, like `123` or `blubb`. | +| argc & argv | Taken from `int main(int argc, char* argv[])`. Provides access to the command line arguments. | + +### Option Types + +| Name | Description | +| :---------------- | :------------------------------------------------------- | +| switch | When provided as argument the switch is activated, see `-h` and `--help`. A boolean signals to the user whether the switch was provided (activated) or not. | +| optional | A value which can be provided via command line but is not required. Every optional option requires a default value. | +| required | A value which has to be provided via command line. Does not require a default value. | + +Every option has an option argument but since the switch is boolean in nature +the argument is implicitly provided when stating the option. + +## Design + +### Considerations + +The solution shall be: + + * easy to use by the developer + * provide a unified appearance and syntax in all applications with command line arguments + * the help must be generated by the parser + * the help shall contain the type if an argument requires a value + * the syntax is defined by the parser not the developer as much as possible + * shall be type safe when command line arguments are converted to types like `int` or `float` + * underflow, overflow shall be handled + * a detailed error explanation shall be presented to the user when cast fails + +### Solution + +#### Class Diagram + +The `CommandLineParser` takes an `OptionDefinition` and parses +the raw command line arguments (`argc` and `argv`) based onto the `OptionDefinition` +into `Arguments` which can be used to access the values of those options. + +![class diagram](../website/images/command_line_parser_class_overview.svg) + +#### Sequence Diagram + +Lets assume we would like to add a switch, an optional and a required option, parse +them and print them on the console. + +![sequence diagram](../website/images/command_line_parser_usage.svg) + +#### Macro Based Code Generator + +```cpp +struct UserCLIStruct +{ + IOX_CLI_DEFINITION(UserCLIStruct, "My program description"); + + IOX_CLI_OPTIONAL(string<100>, stringValue, {"default Value"}, 's', "string-value", "some description"); + IOX_CLI_REQUIRED(string<100>, anotherString, 'a', "another-string", "some description"); + IOX_CLI_SWITCH(uint64_t, version, 0, 'v', "version", "print app version"); +}; + +// This struct parses all command line arguments and stores them. In +// the example above the struct provides access to +// .stringValue() +// .anotherString() +// .version() +// Via the command line parameters +// -s or --string-value +// -a or --another-string +// -v or --version + +int main(int argc, char* argv[]) { + UserCLIStruct cmd(argc, argv); + std::cout << cmd.stringValue() << " " << cmd.anotherString() << std::endl; +} +``` + +The macros `IOX_CLI_DEFINITION`, `IOX_CLI_SWITCH`, `IOX_CLI_OPTIONAL` and `IOX_CLI_REQUIRED` +provide building blocks so that the user can generate a struct. The members of that +struct are defined via the macros and set in the constructor of that struct which +will use the `OptionManager` to parse and extract the +values safely. + +![macro sequence diagram](../website/images/command_line_parser_macro_usage.svg) + +The struct constructor can be called with `argc` and `argv` as arguments from +`int main(int argc, char* argv[])`. + + +## Open issues + +Our `iox` tool should be structured similar like the `ros2` tool. This +means `iox COMMAND ARGUMENTS_OF_THE_COMMAND`. The current implementation allows us +only to parse the `ARGUMENTS_OF_THE_COMMAND` but not handle the `COMMAND` in an easy manner. + +A follow up pull request will address this issue. + +The structure will look like the following (taken from proof of concept): +```cpp +struct Command { + cxx::string command; + cxx::function call; +}; + +// this generates a help with an overview of all available commands which can +// be printed with --help or when a syntax error occurs +bool parseCommand(argc, argv, const vector & availableCommands); + +void userDefinedCommand1(argc, argv) { + // here we use the already implemented CommandLineStruct to parse the + // arguments of the command + // The CommandLineStruct generates the help for the command line arguments + // of that specific command +} + +void userDefinedCommand2(argc, argv) { +} + +int main(int argc, char* argv[]) { + parseCommand(argc, argv, {{"command1", userDefinedCommand1}, + {"anotherCommand", userDefinedCommand2}}); +} +``` + +The idea is to handle every command independently from every other command and +do not define everything in one big command line parser object which would +violate the separation of concerns. diff --git a/doc/design/diagrams/command_line_parser/command_line_parser_class_overview.puml b/doc/design/diagrams/command_line_parser/command_line_parser_class_overview.puml new file mode 100644 index 0000000000..8d31532d1d --- /dev/null +++ b/doc/design/diagrams/command_line_parser/command_line_parser_class_overview.puml @@ -0,0 +1,69 @@ +@startuml + +class OptionDefinition { + +OptionDefinition(programDescription, onFailureCallback) + +addSwitch(..) + +addOptional(..) + +addRequired(..) +} + +class Arguments { + +T get(optionName) + +bool has(optionName) + +BinaryName_t binaryName() +} + +class CommandLineParser { + +Arguments parse(OptionDefinition, ..) +} + +class Option { + +isSwitch() + +hasOptionName(optionName) + +isSameOption(otherOption) + +operator<(otherOption) + +isEmpty() + +longOptionNameDoesStartWithDash() + +shortOptionNameIsEqualDash() + +hasLongOptionName(optionName) + +hasShortOptionName(value) + +hasShortOption() + +hasLongOption() + #shortOption + #longOption + #value +} + +class OptionWithDetails::Details{ + #description + #type + #typeName +} + +class OptionWithDetails { + OptionWithDetails(option, description, type, typeName) + operator<(otherOptionWithDetails) + + #details +} + + +class OptionManager { + +OptionManager(programDescription, onFailureCallback) + +T defineOption(..) + +populateDefinedOptions(..) +} +note "Used only by the macro struct builder IOX_CLI_DEFINITION" as N1 +OptionManager .. N1 + + +OptionWithDetails "1" *-- "1" OptionWithDetails::Details +Option <|--- OptionWithDetails + +Arguments "0..n" *-- "1" Option +OptionDefinition "0..n" *-- "1" OptionWithDetails + +CommandLineParser "1" *-- "1" OptionDefinition : borrows +CommandLineParser "1" *-- "1" Arguments : contains + +@enduml diff --git a/doc/design/diagrams/command_line_parser/command_line_parser_macro_usage.puml b/doc/design/diagrams/command_line_parser/command_line_parser_macro_usage.puml new file mode 100644 index 0000000000..9238beafc7 --- /dev/null +++ b/doc/design/diagrams/command_line_parser/command_line_parser_macro_usage.puml @@ -0,0 +1,45 @@ +@startuml + +participant User +participant UserCLIStruct +participant OptionManager + +== Create a struct named UserCLIStruct to store command line option values == + +rnote right User +IOX_CLI_DEFINITION(UserCLIStruct, ProgramDescription) +end note + +== Define options which are stored in struct members == + +rnote right User +IOX_CLI_OPTIONAL(a ...) +IOX_CLI_REQUIRED(b ...) +IOX_CLI_SWITCH(c ...) +end note + +== Instantiate UserCLIStruct, parse command line arguments and populate members == + +create UserCLIStruct + +User -> UserCLIStruct ++ : UserCLIStruct(argc, argv) + + +UserCLIStruct -> OptionManager ++ : OptionManager(ProgramDescription) +return + +UserCLIStruct -> OptionManager ++ : defineOption(a ...) +return + +UserCLIStruct -> OptionManager ++ : defineOption(b ...) +return + +UserCLIStruct -> OptionManager ++ : defineOption(c ...) +return + +UserCLIStruct -> OptionManager ++ : populateDefinedOptions() +return + +return + +@enduml diff --git a/doc/design/diagrams/command_line_parser/command_line_parser_usage.puml b/doc/design/diagrams/command_line_parser/command_line_parser_usage.puml new file mode 100644 index 0000000000..c1b963e92d --- /dev/null +++ b/doc/design/diagrams/command_line_parser/command_line_parser_usage.puml @@ -0,0 +1,36 @@ +@startuml + +== Create a OptionDefinition and define the application options == + +User -> OptionDefinition ++ : OptionDefinition(programName, onFailureCallback) +return OptionDefinition + +User -> OptionDefinition ++ : addSwitch() +return + +User -> OptionDefinition ++ : addOptional() +return + +User -> OptionDefinition ++ : addRequired() +return + +== Parse command line arguments == + +User -> CommandLineParser ++ : parse(OptionDefinition) +return Arguments + +== Acquire command line option values == + +User -> Arguments ++ : has(switchName) +return bool + +User -> Arguments ++ : get(optionalOptionName) +return TypeName + +User -> Arguments ++ : get(requiredOptionName) +return TypeName + +User -> Arguments ++ : binaryName() +return BinaryName_t + +@enduml diff --git a/doc/website/images/command_line_parser_class_overview.svg b/doc/website/images/command_line_parser_class_overview.svg new file mode 100644 index 0000000000..77a829403b --- /dev/null +++ b/doc/website/images/command_line_parser_class_overview.svg @@ -0,0 +1,14 @@ +OptionDefinitionOptionDefinition(programDescription, onFailureCallback)addSwitch(..)addOptional(..)addRequired(..)ArgumentsT get(optionName)bool has(optionName)BinaryName_t binaryName()CommandLineParserArguments parse(OptionDefinition, ..)OptionshortOptionlongOptionvalueisSwitch()hasOptionName(optionName)isSameOption(otherOption)operator<(otherOption)isEmpty()longOptionNameDoesStartWithDash()shortOptionNameIsEqualDash()hasLongOptionName(optionName)hasShortOptionName(value)hasShortOption()hasLongOption()OptionWithDetails::DetailsdescriptiontypetypeNameOptionWithDetailsdetailsOptionWithDetails(option, description, type, typeName)operator<(otherOptionWithDetails)OptionManagerOptionManager(programDescription, onFailureCallback)T defineOption(..)populateDefinedOptions(..)Used only by the macro struct builder IOX_CLI_DEFINITION110..n10..n1borrows11contains11 \ No newline at end of file diff --git a/doc/website/images/command_line_parser_macro_usage.svg b/doc/website/images/command_line_parser_macro_usage.svg new file mode 100644 index 0000000000..4a85de977b --- /dev/null +++ b/doc/website/images/command_line_parser_macro_usage.svg @@ -0,0 +1 @@ +UserUserUserCLIStructOptionManagerOptionManagerCreate a struct named UserCLIStruct to store command line option valuesIOX_CLI_DEFINITION(UserCLIStruct, ProgramDescription)Define options which are stored in struct membersIOX_CLI_OPTIONAL(a ...)IOX_CLI_REQUIRED(b ...)IOX_CLI_SWITCH(c ...)Instantiate UserCLIStruct, parse command line arguments and populate membersUserCLIStruct(argc, argv)UserCLIStructOptionManager(ProgramDescription)defineOption(a ...)defineOption(b ...)defineOption(c ...)populateDefinedOptions() \ No newline at end of file diff --git a/doc/website/images/command_line_parser_usage.svg b/doc/website/images/command_line_parser_usage.svg new file mode 100644 index 0000000000..e85e6dfa88 --- /dev/null +++ b/doc/website/images/command_line_parser_usage.svg @@ -0,0 +1 @@ +UserUserOptionDefinitionOptionDefinitionCommandLineParserCommandLineParserArgumentsArgumentsCreate a OptionDefinition and define the application optionsOptionDefinition(programName, onFailureCallback)OptionDefinitionaddSwitch()addOptional()addRequired()Parse command line argumentsparse(OptionDefinition)ArgumentsAcquire command line option valueshas(switchName)boolget<TypeName>(optionalOptionName)TypeNameget<TypeName>(requiredOptionName)TypeNamebinaryName()BinaryName_t \ No newline at end of file diff --git a/doc/website/release-notes/iceoryx-unreleased.md b/doc/website/release-notes/iceoryx-unreleased.md index 71060164a3..69d6cf8727 100644 --- a/doc/website/release-notes/iceoryx-unreleased.md +++ b/doc/website/release-notes/iceoryx-unreleased.md @@ -6,6 +6,7 @@ **Features:** +- Add `command_line.hpp` which contains a macro builder to parse command line arguments quickly and safely [#1067](https://github.com/eclipse-iceoryx/iceoryx/issues/1067) - optional inherits from FunctionalInterface, adds .expect() method [\#996](https://github.com/eclipse-iceoryx/iceoryx/issues/996) - Add clear method for `iox::cxx::string` [\#208](https://github.com/eclipse-iceoryx/iceoryx/issues/208) - Add at method and operator[] for `iox::cxx::string` [\#208](https://github.com/eclipse-iceoryx/iceoryx/issues/208) diff --git a/iceoryx_dust/CMakeLists.txt b/iceoryx_dust/CMakeLists.txt index 9f5f3de19d..1eb6874c6f 100644 --- a/iceoryx_dust/CMakeLists.txt +++ b/iceoryx_dust/CMakeLists.txt @@ -48,6 +48,11 @@ iox_add_library( INSTALL_INTERFACE include/${PREFIX} EXPORT_INCLUDE_DIRS include/ FILES + source/cli/arguments.cpp + source/cli/command_line_parser.cpp + source/cli/option.cpp + source/cli/option_definition.cpp + source/cli/option_manager.cpp source/cxx/file_reader.cpp source/posix_wrapper/named_pipe.cpp source/posix_wrapper/signal_watcher.cpp diff --git a/iceoryx_dust/include/iceoryx_dust/cli/command_line_argument_definition.hpp b/iceoryx_dust/include/iceoryx_dust/cli/command_line_argument_definition.hpp new file mode 100644 index 0000000000..86c780eddc --- /dev/null +++ b/iceoryx_dust/include/iceoryx_dust/cli/command_line_argument_definition.hpp @@ -0,0 +1,126 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 +#ifndef IOX_DUST_CLI_COMMAND_LINE_ARGUMENT_DEFINITION_HPP +#define IOX_DUST_CLI_COMMAND_LINE_ARGUMENT_DEFINITION_HPP + +#include "iceoryx_dust/internal/cli/option_manager.hpp" + +#define IOX_INTERNAL_CMD_LINE_VALUE(type, memberName, defaultValue, shortName, longName, description, optionType) \ + public: \ + const type& memberName() const noexcept \ + { \ + return m_##memberName; \ + } \ + \ + private: \ + type m_##memberName = [this] { \ + return this->m_optionManager->defineOption( \ + this->m_##memberName, shortName, longName, description, optionType, defaultValue); \ + }() + +/// @brief Adds an optional value to the command line +/// @param[in] type the type of the optional value +/// @param[in] memberName the name under which the optional value is accessible +/// @param[in] defaultValue the value when it is not set from outside +/// @param[in] shortName a single character for the short option like `-s` for instance +/// @param[in] longName a long option name under which this can be accessed like `--some-name` for instance +/// @param[in] description a description of the optional value +#define IOX_CLI_OPTIONAL(type, memberName, defaultValue, shortName, longName, description) \ + IOX_INTERNAL_CMD_LINE_VALUE( \ + type, memberName, defaultValue, shortName, longName, description, iox::cli::OptionType::OPTIONAL) + +/// @brief Adds a required value to the command line, if it is not provided the program will print the help and +/// terminate +/// @param[in] type the type of the required value +/// @param[in] memberName the name under which the required value is accessible +/// @param[in] shortName a single character for the short option like `-s` for instance +/// @param[in] longName a long option name under which this can be accessed like `--some-name` for instance +/// @param[in] description a description of the required value +#define IOX_CLI_REQUIRED(type, memberName, shortName, longName, description) \ + IOX_INTERNAL_CMD_LINE_VALUE( \ + type, memberName, type(), shortName, longName, description, iox::cli::OptionType::REQUIRED) + +/// @brief Adds a switch to the command line +/// @param[in] memberName the name under which the switch is accessible +/// @param[in] shortName a single character for the short option like `-s` for instance +/// @param[in] longName a long option name under which this can be accessed like `--some-name` for instance +/// @param[in] description a description of the switch +#define IOX_CLI_SWITCH(memberName, shortName, longName, description) \ + IOX_INTERNAL_CMD_LINE_VALUE(bool, memberName, false, shortName, longName, description, iox::cli::OptionType::SWITCH) + +/// @brief Helper macro to create a struct with full command line parsing from argc, argv. +/// @param[in] Name the name of the class/struct +/// @param[in] ProgramDescription a description which describes the task of the program +/// @code +/// // With those macros a struct can be generated easily like this: +/// struct CommandLine +/// { +/// IOX_CLI_DEFINITION(CommandLine); +/// +/// IOX_CLI_OPTIONAL(string<100>, stringValue, {"default Value"}, 's', "string-value", "some description"); +/// IOX_CLI_REQUIRED(string<100>, anotherString, 'a', "another-string", "some description"); +/// IOX_CLI_SWITCH(doStuff, 'd', "do-stuff", "do some stuff - some description"); +/// IOX_CLI_OPTIONAL(uint64_t, version, 0, 'v', "version", "some description"); +/// }; +/// +/// // This struct parses all command line arguments and stores them. In +/// // the example above the struct provides access to +/// // .stringValue() +/// // .anotherString() +/// // .doStuff() +/// // .version() +/// // Via the command line parameters +/// // -s or --string-value +/// // -a or --another-string +/// // -d or --do-stuff +/// // -v or --version +/// +/// int main(int argc, char* argv[]) { +/// auto cmd = CommandLine::parse(argc, argv, "My program description"); +/// std::cout << cmd.stringValue() << " " << cmd.anotherString() << std::endl; +/// } +/// @endcode +#define IOX_CLI_DEFINITION(Name) \ + private: \ + Name(::iox::cli::internal::OptionManager& optionManager, int argc, char* argv[], const uint64_t argcOffset = 1U) \ + : m_optionManager{&optionManager} \ + { \ + m_optionManager->populateDefinedOptions(m_binaryName, argc, argv, argcOffset); \ + } \ + \ + public: \ + static Name parse( \ + int argc, \ + char* argv[], \ + const iox::cli::OptionDescription_t& programDescription, \ + const uint64_t argcOffset = 1U, \ + const ::iox::cxx::function onFailureCallback = [] { std::exit(EXIT_FAILURE); }) \ + { \ + ::iox::cli::internal::OptionManager optionManager(programDescription, onFailureCallback); \ + return Name(optionManager, argc, argv, argcOffset); \ + } \ + \ + const char* binaryName() const noexcept \ + { \ + return m_binaryName; \ + } \ + \ + private: \ + ::iox::cli::internal::OptionManager* m_optionManager = nullptr; \ + const char* m_binaryName = nullptr + + +#endif diff --git a/iceoryx_dust/include/iceoryx_dust/cli/types.hpp b/iceoryx_dust/include/iceoryx_dust/cli/types.hpp new file mode 100644 index 0000000000..ce163cdd6b --- /dev/null +++ b/iceoryx_dust/include/iceoryx_dust/cli/types.hpp @@ -0,0 +1,54 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef IOX_DUST_CLI_TYPES_HPP +#define IOX_DUST_CLI_TYPES_HPP + +#include "iceoryx_hoofs/cxx/string.hpp" +#include "iceoryx_platform/platform_settings.hpp" + +#include + +namespace iox +{ +namespace cli +{ +/// @brief defines the type of command line argument option +enum class OptionType : uint8_t +{ + /// @brief option when provided is true + SWITCH, + /// @brief option with value which has to be provided + REQUIRED, + /// @brief option with value which can be provided + OPTIONAL +}; + +static constexpr uint64_t MAX_OPTION_NAME_LENGTH = 32; +static constexpr uint64_t MAX_OPTION_ARGUMENT_LENGTH = 128; +static constexpr uint64_t MAX_OPTION_DESCRIPTION_LENGTH = 1024; +static constexpr uint64_t MAX_TYPE_NAME_LENGTH = 16; +static constexpr char NO_SHORT_OPTION = '\0'; +static constexpr uint64_t MAX_NUMBER_OF_ARGUMENTS = 16; + +using OptionName_t = cxx::string; +using OptionDescription_t = cxx::string; +using Argument_t = cxx::string; +using TypeName_t = cxx::string; + +} // namespace cli +} // namespace iox +#endif diff --git a/iceoryx_dust/include/iceoryx_dust/internal/cli/arguments.hpp b/iceoryx_dust/include/iceoryx_dust/internal/cli/arguments.hpp new file mode 100644 index 0000000000..218c909638 --- /dev/null +++ b/iceoryx_dust/include/iceoryx_dust/internal/cli/arguments.hpp @@ -0,0 +1,77 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 +#ifndef IOX_DUST_CLI_ARGUMENTS_HPP +#define IOX_DUST_CLI_ARGUMENTS_HPP + +#include "iceoryx_dust/cli/types.hpp" +#include "iceoryx_dust/internal/cli/option.hpp" +#include "iceoryx_hoofs/cxx/convert.hpp" +#include "iceoryx_hoofs/cxx/expected.hpp" +#include "iceoryx_hoofs/cxx/vector.hpp" + +namespace iox +{ +namespace cli +{ +namespace internal +{ +/// @brief This class provides access to the command line argument values. +/// When constructed with the default constructor it is empty. Calling +/// CommandLineParser::parse creates and returns a populated Arguments +/// object. +/// This class should never be used directly. Use the CommandLine builder +/// from `iceoryx_hoofs/cxx/command_line_argument_definition.hpp` to create a struct which contains +/// the values. +class Arguments +{ + public: + enum class Error + { + UNABLE_TO_CONVERT_VALUE, + NO_SUCH_VALUE + }; + + /// @brief returns the value of a specified option + /// @tparam T the type of the value + /// @param[in] optionName either one letter for the shortOption or the whole longOption + /// @return the contained value if the value is present and convertable, otherwise an Error which describes the + /// error + template + cxx::expected get(const OptionName_t& optionName) const noexcept; + + /// @brief returns true if the specified switch was set, otherwise false + /// @param[in] switchName either one letter for the shortOption or the whole longOption + bool isSwitchSet(const OptionName_t& switchName) const noexcept; + + /// @brief returns the full path name of the binary + const char* binaryName() const noexcept; + + private: + template + cxx::expected convertFromString(const Argument_t& value) const noexcept; + friend class CommandLineParser; + + + private: + const char* m_binaryName; + cxx::vector m_arguments; +}; +} // namespace internal +} // namespace cli +} // namespace iox + +#include "iceoryx_dust/internal/cli/arguments.inl" +#endif diff --git a/iceoryx_dust/include/iceoryx_dust/internal/cli/arguments.inl b/iceoryx_dust/include/iceoryx_dust/internal/cli/arguments.inl new file mode 100644 index 0000000000..0e23d29dbe --- /dev/null +++ b/iceoryx_dust/include/iceoryx_dust/internal/cli/arguments.inl @@ -0,0 +1,68 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 +#ifndef IOX_DUST_CLI_COMMAND_LINE_OPTION_VALUE_INL +#define IOX_DUST_CLI_COMMAND_LINE_OPTION_VALUE_INL + +#include "iceoryx_dust/internal/cli/arguments.hpp" + +namespace iox +{ +namespace cli +{ +namespace internal +{ +template +inline cxx::expected Arguments::convertFromString(const Argument_t& stringValue) const noexcept +{ + T value; + if (!cxx::convert::fromString(stringValue.c_str(), value)) + { + std::cout << "\"" << stringValue.c_str() << "\" could not be converted to the requested type" << std::endl; + return cxx::error(Error::UNABLE_TO_CONVERT_VALUE); + } + return cxx::success(value); +} + +template <> +inline cxx::expected Arguments::convertFromString(const Argument_t& stringValue) const noexcept +{ + if (stringValue != "true" && stringValue != "false") + { + std::cout << "\"" << stringValue.c_str() << "\" could not be converted to the requested type" << std::endl; + return cxx::error(Error::UNABLE_TO_CONVERT_VALUE); + } + + return cxx::success(stringValue == "true"); +} + +template +inline cxx::expected Arguments::get(const OptionName_t& optionName) const noexcept +{ + for (const auto& a : m_arguments) + { + if (a.hasOptionName(optionName)) + { + return convertFromString(a.value); + } + } + + return cxx::error(Error::NO_SUCH_VALUE); +} +} // namespace internal +} // namespace cli +} // namespace iox + +#endif diff --git a/iceoryx_dust/include/iceoryx_dust/internal/cli/command_line_parser.hpp b/iceoryx_dust/include/iceoryx_dust/internal/cli/command_line_parser.hpp new file mode 100644 index 0000000000..d476721f20 --- /dev/null +++ b/iceoryx_dust/include/iceoryx_dust/internal/cli/command_line_parser.hpp @@ -0,0 +1,96 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 +#ifndef IOX_DUST_CLI_COMMAND_PARSER_HPP +#define IOX_DUST_CLI_COMMAND_PARSER_HPP + +#include "iceoryx_dust/internal/cli/arguments.hpp" +#include "iceoryx_dust/internal/cli/option_definition.hpp" +#include + +namespace iox +{ +namespace cli +{ +namespace internal +{ +/// @brief Factory class for the CommandLineOption. First, one has to register +/// all switches and options before calling parse. This is required for +/// the help page which is generated and printed on failure as well as +/// for consistency and syntax checks. +class CommandLineParser +{ + public: + static constexpr uint64_t OPTION_OUTPUT_WIDTH = 45; + + private: + friend class OptionManager; + friend Arguments parseCommandLineArguments(const OptionDefinition&, int, char*[], const uint64_t) noexcept; + + /// @brief Parses the arguments from the command line. + /// Calls onFailureCallback in optionSet when the command line arguments contain illegal syntax or required + /// values are not provided and prints the help. + /// @param[in] optionSet the user defined options, based on those options the Arguments object is + /// generated + /// @param[in] argc number of arguments, see int main(int argc, char*argv[]) + /// @param[in] argv the string array of arguments, see int main(int argc, char*argv[]) + /// @param[in] argcOffset the starting point for the parsing. 1U starts at the first argument. + Arguments parse(const OptionDefinition& optionSet, int argc, char* argv[], const uint64_t argcOffset = 1U) noexcept; + + void printHelpAndExit() const noexcept; + + /// BEGIN only used in parse to improve readability + /// + /// Do not use those internal methods outside of parse. They were written + /// to improve the readability of the code. None of those functions verify + /// the pre conditions they require, this has to be done by calling them + /// in the correct order. + bool doesFitIntoString(const char* value, const uint64_t maxLength) const noexcept; + bool areAllRequiredValuesPresent() const noexcept; + bool hasArguments(const uint64_t argc) const noexcept; + bool doesOptionStartWithDash(const char* option) const noexcept; + bool hasNonEmptyOptionName(const char* option) const noexcept; + bool doesNotHaveLongOptionDash(const char* option) const noexcept; + bool doesNotExceedLongOptionDash(const char* option) const noexcept; + bool doesOptionNameFitIntoString(const char* option) const noexcept; + bool isNextArgumentAValue(const uint64_t position) const noexcept; + bool isOptionSet(const OptionWithDetails& entry) const noexcept; + bool doesOptionValueFitIntoString(const char* value) const noexcept; + bool doesOptionHasSucceedingValue(const OptionWithDetails& entry, const uint64_t position) const noexcept; + bool hasLexicallyValidOption(const char* value) const noexcept; + /// END only used in parse to improve readability + + void setDefaultValuesToUnsetOptions() noexcept; + + private: + uint64_t m_argc = 0; + char** m_argv = nullptr; + uint64_t m_argcOffset = 0; + + const OptionDefinition* m_optionSet = nullptr; + Arguments m_optionValue; +}; + +/// @copydoc CommandLineParser::parse() +Arguments parseCommandLineArguments(const OptionDefinition& optionSet, + int argc, + char* argv[], + const uint64_t argcOffset = 1U) noexcept; + +} // namespace internal +} // namespace cli +} // namespace iox + +#endif diff --git a/iceoryx_dust/include/iceoryx_dust/internal/cli/option.hpp b/iceoryx_dust/include/iceoryx_dust/internal/cli/option.hpp new file mode 100644 index 0000000000..0139bff3e6 --- /dev/null +++ b/iceoryx_dust/include/iceoryx_dust/internal/cli/option.hpp @@ -0,0 +1,102 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef IOX_DUST_CLI_OPTION_HPP +#define IOX_DUST_CLI_OPTION_HPP + +#include "iceoryx_dust/cli/types.hpp" + +namespace iox +{ +namespace cli +{ +namespace internal +{ +/// @brief Represents a command line option +struct Option +{ + /// @brief returns true when the name is either equal to the long or + /// short option + /// @param[in] name the option name in question + bool hasOptionName(const OptionName_t& name) const noexcept; + + /// @brief returns true when the long and short options are equal (does + /// not consider value) + /// @param[in] rhs the other option to which it should be compared + bool isSameOption(const Option& rhs) const noexcept; + + /// @brief Returns true if rhs is greater than this. It implements a + /// lexicographical order. + /// @param[in] rhs the other option to which it should be compared + bool operator<(const Option& rhs) const noexcept; + + /// @brief returns true when neither short nor long option is set + bool isEmpty() const noexcept; + + /// @brief returns true when the long options starts with dash + bool longOptionNameDoesStartWithDash() const noexcept; + + /// @brief returns true when the short option is a dash + bool shortOptionNameIsEqualDash() const noexcept; + + /// @brief returns true when the long option name is equal to value + /// @param[in] value the option name in question + bool hasLongOptionName(const OptionName_t& value) const noexcept; + + /// @brief returns true when the short option name is equal to value + /// @param[in] value the option name in question + bool hasShortOptionName(const char value) const noexcept; + + /// @brief returns true when it contains a short option which is set + bool hasShortOption() const noexcept; + + /// @brief returns true when it contains a long option which is set + bool hasLongOption() const noexcept; + + char shortOption = NO_SHORT_OPTION; + bool isSwitch = false; + OptionName_t longOption; + Argument_t value; +}; + +struct OptionWithDetails : public Option // can this be melt together +{ + /// @brief construct a Option class with additional details + /// @param[in] option the option + /// @param[in] description the description of the option + /// @param[in] type the type of the option + /// @param[in] typeName the type name of the option + OptionWithDetails(const Option& option, + const OptionDescription_t& description, + const OptionType type, + const TypeName_t& typeName) noexcept; + + /// @brief Returns true if rhs is greater than this. It implements a + /// lexicographical order. + /// @param[in] rhs the other OptionWithDetails to which it should be compared + bool operator<(const OptionWithDetails& rhs) const noexcept; + + struct + { + OptionDescription_t description; + OptionType type = OptionType::SWITCH; + TypeName_t typeName; + } details; +}; +} // namespace internal +} // namespace cli +} // namespace iox +#endif diff --git a/iceoryx_dust/include/iceoryx_dust/internal/cli/option_definition.hpp b/iceoryx_dust/include/iceoryx_dust/internal/cli/option_definition.hpp new file mode 100644 index 0000000000..49d1d294f9 --- /dev/null +++ b/iceoryx_dust/include/iceoryx_dust/internal/cli/option_definition.hpp @@ -0,0 +1,101 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef IOX_DUST_CLI_OPTION_DEFINITION_HPP +#define IOX_DUST_CLI_OPTION_DEFINITION_HPP + +#include "iceoryx_dust/cli/types.hpp" +#include "iceoryx_dust/internal/cli/arguments.hpp" +#include "iceoryx_hoofs/cxx/function.hpp" +#include "iceoryx_hoofs/cxx/vector.hpp" +#include + +namespace iox +{ +namespace cli +{ +namespace internal +{ +/// @brief A set of options which is provided to the CommandLineParser. +/// Description, short and long name as well as type and value can be defined for every +/// command line option which the application provides. +/// The parser uses this set to populate the Arguments. +class OptionDefinition +{ + public: + /// @brief The constructor. + /// @param[in] programDescription The description to the program. Will be printed in the help. + /// @param[in] onFailureCallback callback which is called when parse fails, if nothing is + /// defined std::exit(EXIT_FAILURE) is called + explicit OptionDefinition( + const OptionDescription_t& programDescription, + const cxx::function onFailureCallback = [] { std::exit(EXIT_FAILURE); }) noexcept; + + /// @brief Adds a command line switch argument + /// Calls the onFailureCallback when the option was already added or the shortOption and longOption are + /// empty. + /// @param[in] shortOption a single letter as short option + /// @param[in] longOption a multi letter word which does not start with dash as long option name + /// @param[in] description the description to the argument + OptionDefinition& + addSwitch(const char shortOption, const OptionName_t& longOption, const OptionDescription_t& description) noexcept; + + /// @brief Adds a command line optional value argument. + /// Calls the onFailureCallback when the option was already added or the shortOption and longOption are + /// empty. + /// @param[in] shortOption a single letter as short option + /// @param[in] longOption a multi letter word which does not start with dash as long option name + /// @param[in] description the description to the argument + /// @param[in] typeName the name of the value type + /// @param[in] defaultValue the value which will be set to the option when it is not set by the user + OptionDefinition& addOptional(const char shortOption, + const OptionName_t& longOption, + const OptionDescription_t& description, + const TypeName_t& typeName, + const Argument_t& defaultValue) noexcept; + + /// @brief Adds a command line required value argument + /// Calls the onFailureCallback when the option was already added or the shortOption and longOption are + /// empty. + /// @param[in] shortOption a single letter as short option + /// @param[in] longOption a multi letter word which does not start with dash as long option name + /// @param[in] description the description to the argument + /// @param[in] typeName the name of the value type + OptionDefinition& addRequired(const char shortOption, + const OptionName_t& longOption, + const OptionDescription_t& description, + const TypeName_t& typeName) noexcept; + + private: + friend class OptionManager; + friend class CommandLineParser; + friend std::ostream& operator<<(std::ostream&, const OptionWithDetails&) noexcept; + + OptionDefinition& addOption(const OptionWithDetails& option) noexcept; + cxx::optional getOption(const OptionName_t& name) const noexcept; + + private: + OptionDescription_t m_programDescription; + cxx::vector m_availableOptions; + cxx::function m_onFailureCallback; +}; + +std::ostream& operator<<(std::ostream& stream, const OptionWithDetails& value) noexcept; +} // namespace internal +} // namespace cli +} // namespace iox + +#endif diff --git a/iceoryx_dust/include/iceoryx_dust/internal/cli/option_manager.hpp b/iceoryx_dust/include/iceoryx_dust/internal/cli/option_manager.hpp new file mode 100644 index 0000000000..bebb6d142d --- /dev/null +++ b/iceoryx_dust/include/iceoryx_dust/internal/cli/option_manager.hpp @@ -0,0 +1,89 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 +#ifndef IOX_DUST_CLI_OPTION_MANAGER_HPP +#define IOX_DUST_CLI_OPTION_MANAGER_HPP + +#include "iceoryx_dust/internal/cli/command_line_parser.hpp" +#include "iceoryx_dust/internal/cli/option_definition.hpp" +#include "iceoryx_hoofs/cxx/function.hpp" +#include "iceoryx_hoofs/cxx/vector.hpp" + +namespace iox +{ +namespace cli +{ +namespace internal +{ +using CmdAssignments_t = cxx::vector, MAX_NUMBER_OF_ARGUMENTS>; + +/// @brief Manages command line options which were defined via the IOX_CLI_ macros in a +/// user defined struct. +/// This class ensures that the option values are assigned to the member variables +/// of the struct when the constructor of the IOX_CLI_DEFINITION struct is called. +class OptionManager +{ + public: + /// @brief Create OptionManager + /// @param[in] programDescription the description of the application + /// @param[in] onFailureCallback callback which is called when a syntax error occurs, a required option is missing + /// or the wrong type as argument value is provided + OptionManager(const OptionDescription_t& programDescription, const cxx::function onFailureCallback); + + /// @brief Defines a new option + /// @param[in] referenceToMember an uninitialized piece of memory where later the content is stored when + /// populateDefinedOptions is called + /// @param[in] shortName the short option name + /// @param[in] name the long option name + /// @param[in] description the description of the option + /// @param[in] optionType the type of option + /// @param[in] defaultArgumentValue the default value of the option + template + T defineOption(T& referenceToMember, // not a pointer since it must be always valid + const char shortName, + const OptionName_t& name, + const OptionDescription_t& description, + const OptionType optionType, + T defaultArgumentValue // not const to enable RTVO + ); + + /// @brief populates all defined options + /// @param[in] binaryName the name of the binary + /// @param[in] argc the argument count taken from int main(int argc, char*argv[]) + /// @param[in] argv the argument array ptr taken from int main(int argc, char*argv[]) + /// @param[in] argcOffset the offset from which the arguments should be parsed + void populateDefinedOptions(const char*& binaryName, int argc, char* argv[], const uint64_t argcOffset); + + private: + CommandLineParser m_parser; + OptionDefinition m_optionSet; + CmdAssignments_t m_assignments; + + private: + static OptionName_t getLookupName(const char shortName, const OptionName_t& name) noexcept; + + template + T extractOptionArgumentValue(const Arguments& arguments, + const char shortName, + const OptionName_t& name, + const OptionType optionType); +}; + +} // namespace internal +} // namespace cli +} // namespace iox + +#include "iceoryx_dust/internal/cli/option_manager.inl" +#endif diff --git a/iceoryx_dust/include/iceoryx_dust/internal/cli/option_manager.inl b/iceoryx_dust/include/iceoryx_dust/internal/cli/option_manager.inl new file mode 100644 index 0000000000..c91cb059e2 --- /dev/null +++ b/iceoryx_dust/include/iceoryx_dust/internal/cli/option_manager.inl @@ -0,0 +1,83 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef IOX_DUST_CLI_OPTION_MANAGER_INL +#define IOX_DUST_CLI_OPTION_MANAGER_INL + +#include "iceoryx_dust/internal/cli/option_manager.hpp" + +namespace iox +{ +namespace cli +{ +namespace internal +{ +template +inline T OptionManager::extractOptionArgumentValue(const Arguments& arguments, + const char shortName, + const OptionName_t& name, + const OptionType) +{ + return arguments.get(getLookupName(shortName, name)) + .or_else([this](auto&) { m_parser.printHelpAndExit(); }) + .value(); +} + +template <> +inline bool OptionManager::extractOptionArgumentValue(const Arguments& arguments, + const char shortName, + const OptionName_t& name, + const OptionType optionType) +{ + if (optionType == OptionType::SWITCH) + { + return arguments.isSwitchSet(getLookupName(shortName, name)); + } + + return arguments.get(getLookupName(shortName, name)) + .or_else([this](auto&) { m_parser.printHelpAndExit(); }) + .value(); +} + +template +inline T OptionManager::defineOption(T& referenceToMember, + const char shortName, + const OptionName_t& name, + const OptionDescription_t& description, + const OptionType optionType, + T defaultArgumentValue) +{ + constexpr bool IS_NO_SWITCH = false; + m_optionSet.addOption( + OptionWithDetails{{shortName, + IS_NO_SWITCH, + name, + Argument_t(cxx::TruncateToCapacity, cxx::convert::toString(defaultArgumentValue))}, + description, + optionType, + {cxx::TypeInfo::NAME}}); + + m_assignments.emplace_back([this, &referenceToMember, optionType, shortName, name](Arguments& arguments) { + referenceToMember = extractOptionArgumentValue(arguments, shortName, name, optionType); + }); + + return defaultArgumentValue; +} +} // namespace internal +} // namespace cli +} // namespace iox + +#endif diff --git a/iceoryx_dust/source/cli/arguments.cpp b/iceoryx_dust/source/cli/arguments.cpp new file mode 100644 index 0000000000..b9e56c2f9b --- /dev/null +++ b/iceoryx_dust/source/cli/arguments.cpp @@ -0,0 +1,43 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_dust/internal/cli/arguments.hpp" + +namespace iox +{ +namespace cli +{ +namespace internal +{ +const char* Arguments::binaryName() const noexcept +{ + return m_binaryName; +} + +bool Arguments::isSwitchSet(const OptionName_t& switchName) const noexcept +{ + for (const auto& a : m_arguments) + { + if (a.isSwitch && a.hasOptionName(switchName)) + { + return true; + } + } + return false; +} +} // namespace internal +} // namespace cli +} // namespace iox diff --git a/iceoryx_dust/source/cli/command_line_parser.cpp b/iceoryx_dust/source/cli/command_line_parser.cpp new file mode 100644 index 0000000000..81fa518c5a --- /dev/null +++ b/iceoryx_dust/source/cli/command_line_parser.cpp @@ -0,0 +1,381 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_dust/internal/cli/command_line_parser.hpp" +#include "iceoryx_hoofs/cxx/algorithm.hpp" +#include "iceoryx_hoofs/error_handling/error_handling.hpp" + +#include + +namespace iox +{ +namespace cli +{ +namespace internal +{ +Arguments +parseCommandLineArguments(const OptionDefinition& optionSet, int argc, char* argv[], const uint64_t argcOffset) noexcept +{ + return CommandLineParser().parse(optionSet, argc, argv, argcOffset); +} + +bool CommandLineParser::hasArguments(const uint64_t argc) const noexcept +{ + const bool hasArguments = (argc > 0); + if (!hasArguments) + { + printHelpAndExit(); + } + return hasArguments; +} + +bool CommandLineParser::doesOptionStartWithDash(const char* option) const noexcept +{ + const bool doesOptionStartWithDash = (strnlen(option, 1) > 0 && option[0] == '-'); + + if (!doesOptionStartWithDash) + { + std::cout << "Every option has to start with \"-\" but \"" << option << "\" does not." << std::endl; + printHelpAndExit(); + } + return doesOptionStartWithDash; +} + +bool CommandLineParser::hasNonEmptyOptionName(const char* option) const noexcept +{ + const uint64_t minArgIdentifierLength = strnlen(option, 3); + const bool hasNonEmptyOptionName = + !(minArgIdentifierLength == 1 || (minArgIdentifierLength == 2 && option[1] == '-')); + + if (!hasNonEmptyOptionName) + { + std::cout << "Empty option names are forbidden" << std::endl; + printHelpAndExit(); + } + + return hasNonEmptyOptionName; +} + +bool CommandLineParser::doesNotHaveLongOptionDash(const char* option) const noexcept +{ + const uint64_t minArgIdentifierLength = strnlen(option, 3); + const bool doesNotHaveLongOptionDash = !(minArgIdentifierLength > 2 && option[1] != '-'); + + if (!doesNotHaveLongOptionDash) + { + std::cout << "Only one letter allowed when using a short option name. The switch \"" << option + << "\" is not valid." << std::endl; + printHelpAndExit(); + } + return doesNotHaveLongOptionDash; +} + +bool CommandLineParser::doesNotExceedLongOptionDash(const char* option) const noexcept +{ + const uint64_t minArgIdentifierLength = strnlen(option, 3); + const bool doesNotExceedLongOptionDash = !(minArgIdentifierLength > 2 && option[2] == '-'); + + if (!doesNotExceedLongOptionDash) + { + std::cout << "A long option name should start after \"--\". This \"" << option << "\" is not valid." + << std::endl; + printHelpAndExit(); + } + return doesNotExceedLongOptionDash; +} + +bool CommandLineParser::doesFitIntoString(const char* value, const uint64_t maxLength) const noexcept +{ + return (strnlen(value, maxLength + 1) <= maxLength); +} + +bool CommandLineParser::doesOptionNameFitIntoString(const char* option) const noexcept +{ + const bool doesOptionNameFitIntoString = doesFitIntoString(option, MAX_OPTION_NAME_LENGTH); + + if (!doesOptionNameFitIntoString) + { + std::cout << "\"" << option << "\" is longer then the maximum supported size of " << MAX_OPTION_NAME_LENGTH + << " for option names." << std::endl; + printHelpAndExit(); + } + return doesOptionNameFitIntoString; +} + +bool CommandLineParser::isNextArgumentAValue(const uint64_t position) const noexcept +{ + uint64_t nextPosition = position + 1; + return (m_argc > 0 && m_argc > nextPosition + && (strnlen(m_argv[nextPosition], MAX_OPTION_NAME_LENGTH) > 0 && m_argv[nextPosition][0] != '-')); +} + +bool CommandLineParser::isOptionSet(const OptionWithDetails& value) const noexcept +{ + bool isOptionSet = false; + for (const auto& option : m_optionValue.m_arguments) + { + if (option.isSameOption(value)) + { + isOptionSet = true; + break; + } + } + + if (isOptionSet) + { + std::cout << "The option \"" << value << "\" is already set!" << std::endl; + printHelpAndExit(); + } + + return isOptionSet; +} + +bool CommandLineParser::doesOptionValueFitIntoString(const char* value) const noexcept +{ + const bool doesOptionValueFitIntoString = doesFitIntoString(value, MAX_OPTION_ARGUMENT_LENGTH); + + if (!doesOptionValueFitIntoString) + { + std::cout << "\"" << value << "\" is longer then the maximum supported size of " << MAX_OPTION_ARGUMENT_LENGTH + << " for option values." << std::endl; + printHelpAndExit(); + } + + return doesOptionValueFitIntoString; +} + +bool CommandLineParser::hasLexicallyValidOption(const char* value) const noexcept +{ + return doesOptionStartWithDash(value) && hasNonEmptyOptionName(value) && doesNotHaveLongOptionDash(value) + && doesNotExceedLongOptionDash(value) && doesOptionNameFitIntoString(value); +} + +Arguments +CommandLineParser::parse(const OptionDefinition& optionSet, int argc, char* argv[], const uint64_t argcOffset) noexcept +{ + m_optionSet = &optionSet; + + m_argc = static_cast(algorithm::maxVal(0, argc)); + m_argv = argv; + m_argcOffset = argcOffset; + // reset options otherwise multiple parse calls work on already parsed options + m_optionValue = Arguments(); + + if (!hasArguments(m_argc)) + { + return m_optionValue; + } + + m_optionValue.m_binaryName = m_argv[0]; + + for (uint64_t i = algorithm::maxVal(argcOffset, static_cast(1U)); i < m_argc; ++i) + { + const auto skipCommandLineArgument = [&] { ++i; }; + + if (!hasLexicallyValidOption(m_argv[i])) + { + return m_optionValue; + } + + uint64_t optionNameStart = (m_argv[i][1] == '-') ? 2 : 1; + auto optionEntry = m_optionSet->getOption(OptionName_t(cxx::TruncateToCapacity, m_argv[i] + optionNameStart)); + + if (!optionEntry) + { + std::cout << "Unknown option \"" << m_argv[i] << "\"" << std::endl; + printHelpAndExit(); + return m_optionValue; + } + + if (isOptionSet(*optionEntry)) + { + return m_optionValue; + } + + if (optionEntry->details.type == OptionType::SWITCH) + { + m_optionValue.m_arguments.emplace_back(*optionEntry); + m_optionValue.m_arguments.back().value.clear(); + m_optionValue.m_arguments.back().isSwitch = true; + } + else + { + if (!doesOptionHasSucceedingValue(*optionEntry, i)) + { + return m_optionValue; + } + + if (!doesOptionValueFitIntoString(m_argv[i + 1])) + { + return m_optionValue; + } + + m_optionValue.m_arguments.emplace_back(*optionEntry); + m_optionValue.m_arguments.back().value.unsafe_assign(m_argv[i + 1]); + m_optionValue.m_arguments.back().isSwitch = false; + skipCommandLineArgument(); + } + } + + setDefaultValuesToUnsetOptions(); + + if (m_optionValue.isSwitchSet("help") || !areAllRequiredValuesPresent()) + { + printHelpAndExit(); + return m_optionValue; + } + + return m_optionValue; +} + +bool CommandLineParser::doesOptionHasSucceedingValue(const OptionWithDetails& value, + const uint64_t position) const noexcept +{ + bool doesOptionHasSucceedingValue = (position + 1 < m_argc); + if (!doesOptionHasSucceedingValue) + { + std::cout << "The option \"" << value << "\" must be followed by a value!" << std::endl; + printHelpAndExit(); + } + return doesOptionHasSucceedingValue; +} + + +void CommandLineParser::setDefaultValuesToUnsetOptions() noexcept // rename +{ + for (const auto& availableOption : m_optionSet->m_availableOptions) + { + if (availableOption.details.type != OptionType::OPTIONAL) + { + continue; + } + + bool isOptionAlreadySet = false; + for (auto& option : m_optionValue.m_arguments) + { + if (option.isSameOption(availableOption)) + { + isOptionAlreadySet = true; + break; + } + } + + if (!isOptionAlreadySet) + { + m_optionValue.m_arguments.emplace_back(availableOption); + } + } +} + +bool CommandLineParser::areAllRequiredValuesPresent() const noexcept +{ + bool areAllRequiredValuesPresent = true; + for (const auto& availableOption : m_optionSet->m_availableOptions) + { + if (availableOption.details.type == OptionType::REQUIRED) + { + bool isValuePresent = false; + for (const auto& option : m_optionValue.m_arguments) + { + if (option.isSameOption(availableOption)) + { + isValuePresent = true; + break; + } + } + if (!isValuePresent) + { + std::cout << "Required option \"" << availableOption << "\" is unset!" << std::endl; + areAllRequiredValuesPresent = false; + } + } + } + + return areAllRequiredValuesPresent; +} + +void CommandLineParser::printHelpAndExit() const noexcept +{ + std::cout << "\n" << m_optionSet->m_programDescription << "\n" << std::endl; + std::cout << "Usage: "; + for (uint64_t i = 0; i < m_argcOffset && i < m_argc; ++i) + { + std::cout << m_argv[i] << " "; + } + std::cout << "[OPTIONS]\n" << std::endl; + + std::cout << " Options:" << std::endl; + + auto sortedAvailableOptions = m_optionSet->m_availableOptions; + std::sort(sortedAvailableOptions.begin(), sortedAvailableOptions.end()); + + for (const auto& option : sortedAvailableOptions) + { + uint64_t outLength = 4U; + std::cout << " "; + if (option.hasShortOption()) + { + std::cout << "-" << option.shortOption; + outLength += 2; + } + + if (option.hasShortOption() && option.hasLongOption()) + { + std::cout << ", "; + outLength += 2; + } + + if (option.hasLongOption()) + { + std::cout << "--" << option.longOption.c_str(); + outLength += 2 + option.longOption.size(); + } + + if (option.details.type == OptionType::REQUIRED) + { + std::cout << " [" << option.details.typeName << "]"; + outLength += 3 + option.details.typeName.size(); + } + else if (option.details.type == OptionType::OPTIONAL) + { + std::cout << " [" << option.details.typeName << "]"; + outLength += 3 + option.details.typeName.size(); + } + + uint64_t spacing = (outLength + 1 < OPTION_OUTPUT_WIDTH) ? OPTION_OUTPUT_WIDTH - outLength : 2; + + for (uint64_t i = 0; i < spacing; ++i) + { + std::cout << " "; + } + std::cout << option.details.description << std::endl; + + if (option.details.type == OptionType::OPTIONAL) + { + for (uint64_t i = 0; i < OPTION_OUTPUT_WIDTH; ++i) + { + std::cout << " "; + } + std::cout << "default value = \'" << option.value << "\'" << std::endl; + } + } + std::cout << std::endl; + m_optionSet->m_onFailureCallback(); +} + +} // namespace internal +} // namespace cli +} // namespace iox diff --git a/iceoryx_dust/source/cli/option.cpp b/iceoryx_dust/source/cli/option.cpp new file mode 100644 index 0000000000..bf259aeb92 --- /dev/null +++ b/iceoryx_dust/source/cli/option.cpp @@ -0,0 +1,103 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_dust/internal/cli/option.hpp" + +namespace iox +{ +namespace cli +{ +namespace internal +{ +bool Option::isEmpty() const noexcept +{ + return longOption.empty() && shortOption == NO_SHORT_OPTION; +} + +bool Option::longOptionNameDoesStartWithDash() const noexcept +{ + return !longOption.empty() && longOption[0] == '-'; +} + +bool Option::shortOptionNameIsEqualDash() const noexcept +{ + return shortOption == '-'; +} + +bool Option::hasLongOptionName(const OptionName_t& value) const noexcept +{ + return (!longOption.empty() && longOption == value); +} + +bool Option::hasShortOptionName(const char value) const noexcept +{ + return (shortOption != NO_SHORT_OPTION && shortOption == value); +} + +bool Option::hasOptionName(const OptionName_t& name) const noexcept +{ + return hasLongOptionName(name) || (name.size() == 1 && hasShortOptionName(name[0])); +} + +bool Option::isSameOption(const Option& rhs) const noexcept +{ + return (shortOption == rhs.shortOption && longOption == rhs.longOption); +} + +bool Option::hasShortOption() const noexcept +{ + return (shortOption != NO_SHORT_OPTION); +} + +bool Option::hasLongOption() const noexcept +{ + return !longOption.empty(); +} + +bool Option::operator<(const Option& rhs) const noexcept +{ + if (shortOption != NO_SHORT_OPTION && rhs.shortOption != NO_SHORT_OPTION) + { + return shortOption < rhs.shortOption; + } + else if (!longOption.empty() && rhs.shortOption != NO_SHORT_OPTION) + { + return longOption.c_str()[0] < rhs.shortOption; + } + else if (shortOption != NO_SHORT_OPTION && !rhs.longOption.empty()) + { + return shortOption < rhs.longOption.c_str()[0]; + } + + return longOption < rhs.longOption; +} + +OptionWithDetails::OptionWithDetails(const Option& option, + const OptionDescription_t& description, + const OptionType type, + const TypeName_t& typeName) noexcept + : Option{option} + , details{description, type, typeName} +{ +} + +bool OptionWithDetails::operator<(const OptionWithDetails& rhs) const noexcept +{ + return Option::operator<(rhs); +} +} // namespace internal +} // namespace cli +} // namespace iox diff --git a/iceoryx_dust/source/cli/option_definition.cpp b/iceoryx_dust/source/cli/option_definition.cpp new file mode 100644 index 0000000000..9c0ee1db61 --- /dev/null +++ b/iceoryx_dust/source/cli/option_definition.cpp @@ -0,0 +1,145 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_dust/internal/cli/option_definition.hpp" + +namespace iox +{ +namespace cli +{ +namespace internal +{ +OptionDefinition::OptionDefinition(const OptionDescription_t& programDescription, + const cxx::function onFailureCallback) noexcept + : m_programDescription{programDescription} + , m_onFailureCallback{onFailureCallback} +{ + constexpr bool IS_SWITCH = true; + std::move(*this).addOption({{'h', IS_SWITCH, {"help"}, {""}}, {"Display help."}, OptionType::SWITCH, {""}}); +} + +cxx::optional OptionDefinition::getOption(const OptionName_t& name) const noexcept +{ + for (const auto& r : m_availableOptions) + { + if (r.hasOptionName(name)) + { + return r; + } + } + return cxx::nullopt; +} + +OptionDefinition& OptionDefinition::addOption(const OptionWithDetails& option) noexcept +{ + if (option.isEmpty()) + { + std::cout << "Unable to add option with empty short and long option." << std::endl; + m_onFailureCallback(); + return *this; + } + + if (option.longOptionNameDoesStartWithDash()) + { + std::cout << "The first character of a long option cannot start with dash \"-\" but the option \"" + << option.longOption << "\" starts with dash." << std::endl; + m_onFailureCallback(); + return *this; + } + + if (option.shortOptionNameIsEqualDash()) + { + std::cout << "Dash \"-\" is not a valid character for a short option." << std::endl; + m_onFailureCallback(); + return *this; + } + + for (const auto& registeredOption : m_availableOptions) + { + bool isLongOrShortOptionRegistered = false; + if (registeredOption.hasLongOptionName(option.longOption)) + { + std::cout << "The longOption \"--" << registeredOption.longOption << "\" is already registered for option " + << registeredOption << ". Cannot add option \"" << option << "\"." << std::endl; + isLongOrShortOptionRegistered = true; + } + + if (registeredOption.hasShortOptionName(option.shortOption)) + { + std::cout << "The shortOption \"-" << registeredOption.shortOption << "\" is already registered for option " + << registeredOption << ". Cannot add option \"" << option << "\"." << std::endl; + isLongOrShortOptionRegistered = true; + } + + if (isLongOrShortOptionRegistered) + { + m_onFailureCallback(); + return *this; + } + } + + m_availableOptions.emplace_back(option); + + return *this; +} + +OptionDefinition& OptionDefinition::addSwitch(const char shortOption, + const OptionName_t& longOption, + const OptionDescription_t& description) noexcept +{ + constexpr bool IS_SWITCH = true; + return addOption({{shortOption, IS_SWITCH, longOption, {""}}, description, OptionType::SWITCH, {""}}); +} + +OptionDefinition& OptionDefinition::addOptional(const char shortOption, + const OptionName_t& longOption, + const OptionDescription_t& description, + const TypeName_t& typeName, + const Argument_t& defaultValue) noexcept +{ + constexpr bool IS_NO_SWITCH = false; + return addOption( + {{shortOption, IS_NO_SWITCH, longOption, defaultValue}, description, OptionType::OPTIONAL, typeName}); +} +OptionDefinition& OptionDefinition::addRequired(const char shortOption, + const OptionName_t& longOption, + const OptionDescription_t& description, + const TypeName_t& typeName) noexcept +{ + constexpr bool IS_NO_SWITCH = false; + return addOption({{shortOption, IS_NO_SWITCH, longOption, {""}}, description, OptionType::REQUIRED, typeName}); +} + +std::ostream& operator<<(std::ostream& stream, const OptionWithDetails& option) noexcept +{ + if (option.hasShortOption()) + { + stream << "-" << option.shortOption; + } + if (option.hasShortOption() && option.hasLongOption()) + { + stream << ", "; + } + if (option.hasLongOption()) + { + stream << "--" << option.longOption; + } + + return stream; +} +} // namespace internal +} // namespace cli +} // namespace iox diff --git a/iceoryx_dust/source/cli/option_manager.cpp b/iceoryx_dust/source/cli/option_manager.cpp new file mode 100644 index 0000000000..2690b6a735 --- /dev/null +++ b/iceoryx_dust/source/cli/option_manager.cpp @@ -0,0 +1,55 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_dust/internal/cli/option_manager.hpp" + +namespace iox +{ +namespace cli +{ +namespace internal +{ +OptionManager::OptionManager(const OptionDescription_t& programDescription, + const cxx::function onFailureCallback) + : m_optionSet{programDescription, onFailureCallback} +{ +} + +void OptionManager::populateDefinedOptions(const char*& binaryName, int argc, char* argv[], const uint64_t argcOffset) +{ + auto options = m_parser.parse(m_optionSet, argc, argv, argcOffset); + + for (const auto& assignment : m_assignments) + { + assignment(options); + } + + binaryName = options.binaryName(); +} + +OptionName_t OptionManager::getLookupName(const char shortName, const OptionName_t& name) noexcept +{ + if (shortName == NO_SHORT_OPTION) + { + return OptionName_t{cxx::TruncateToCapacity, &shortName, 1}; + } + + return name; +} + +} // namespace internal +} // namespace cli +} // namespace iox diff --git a/iceoryx_dust/test/moduletests/test_cli_command_line_argument_definition.cpp b/iceoryx_dust/test/moduletests/test_cli_command_line_argument_definition.cpp new file mode 100644 index 0000000000..a7d856a2b3 --- /dev/null +++ b/iceoryx_dust/test/moduletests/test_cli_command_line_argument_definition.cpp @@ -0,0 +1,181 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_dust/cli/command_line_argument_definition.hpp" +#include "iceoryx_hoofs/error_handling/error_handling.hpp" +#include "test.hpp" +#include "test_cli_command_line_common.hpp" + +#include +#include +#include +#include + +namespace +{ +using namespace ::testing; +using namespace iox::cxx; + +/// This test is only some kind of compilation test to verify that the +/// command line parser macros are working and connecting everything together correctly +/// The actual test of the command line parser can be found in +/// test_cli_command_line_argument_parser.cpp +class CommandLineArgumentDefinition_test : public Test +{ + public: + void SetUp() override + { + // if we do not capture stdout then the console is filled with garbage + // since the command line parser prints the help on failure + ::testing::internal::CaptureStdout(); + } + void TearDown() override + { + std::string output = ::testing::internal::GetCapturedStdout(); + if (Test::HasFailure()) + { + std::cout << output << std::endl; + } + } +}; + +struct CommandLineArgumentDefinitionSut +{ + IOX_CLI_DEFINITION(CommandLineArgumentDefinitionSut); + + IOX_CLI_OPTIONAL(string<100>, stringValue1, {"default value"}, 's', {"string-value-1"}, {"some description"}); + IOX_CLI_OPTIONAL(string<100>, stringValue2, {"some other value"}, 't', {"string-value-2"}, {"some description"}); + IOX_CLI_OPTIONAL(int64_t, optionalInt1, 123, 'i', {"int-value-1"}, {"some description"}); + IOX_CLI_OPTIONAL(int64_t, optionalInt2, 456, 'j', {"int-value-2"}, {"some description"}); + IOX_CLI_OPTIONAL(uint8_t, optionalUint1, 123, 'u', {"uint-value-1"}, {"some description"}); + IOX_CLI_OPTIONAL(uint8_t, optionalUint2, 212, 'v', {"uint-value-2"}, {"some description"}); + + IOX_CLI_SWITCH(lightSwitch1, 'l', "light-switch-1", "do some stuff - some description"); + IOX_CLI_SWITCH(lightSwitch2, 'm', "light-switch-2", "do some stuff - some description"); + + IOX_CLI_REQUIRED(string<100>, requiredString, 'r', "required-string", "some description"); + IOX_CLI_REQUIRED(float, requiredFloat, 'b', "required-float", "some description"); + IOX_CLI_REQUIRED(uint16_t, requiredUint, 'c', "required-uint", "some description"); +}; + +TEST_F(CommandLineArgumentDefinition_test, OnlyRequiredValuesSetsRemainingValuesToDefault) +{ + ::testing::Test::RecordProperty("TEST_ID", "451701b8-061f-4e30-9beb-1c09c7e6bc1b"); + CmdArgs args( + {"myBinaryName", "--required-string", "bluubb", "--required-float", "123.456", "--required-uint", "12"}); + auto sut = CommandLineArgumentDefinitionSut::parse(args.argc, args.argv, "My program description"); + + EXPECT_THAT(sut.binaryName(), StrEq("myBinaryName")); + + // default values + EXPECT_THAT(sut.stringValue1().c_str(), StrEq("default value")); + EXPECT_THAT(sut.stringValue2().c_str(), StrEq("some other value")); + EXPECT_THAT(sut.optionalInt1(), Eq(123)); + EXPECT_THAT(sut.optionalInt2(), Eq(456)); + EXPECT_THAT(sut.optionalUint1(), Eq(123)); + EXPECT_THAT(sut.optionalUint2(), Eq(212)); + EXPECT_THAT(sut.lightSwitch1(), Eq(false)); + EXPECT_THAT(sut.lightSwitch2(), Eq(false)); + + EXPECT_THAT(sut.requiredString().c_str(), StrEq("bluubb")); + EXPECT_THAT(sut.requiredFloat(), Eq(123.456F)); + EXPECT_THAT(sut.requiredUint(), Eq(12)); +} + +TEST_F(CommandLineArgumentDefinition_test, AllValuesViaCommandLineArgumentDefinitionAreSetCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "0478575e-8eb4-4983-93bd-199d222e706e"); + CmdArgs args({"anotherOneBitesTheDust", + "--required-string", + "schnappidububa", + "--required-float", + "456.123", + "--required-uint", + "1212", + "--string-value-1", + "flatterdude", + "--string-value-2", + "evilhuhn", + "--int-value-1", + "4711123", + "--int-value-2", + "810456", + "--uint-value-1", + "39", + "--uint-value-2", + "31", + "--light-switch-1", + "--light-switch-2"}); + auto sut = CommandLineArgumentDefinitionSut::parse(args.argc, args.argv, "My program description"); + + EXPECT_THAT(sut.binaryName(), StrEq("anotherOneBitesTheDust")); + + EXPECT_THAT(sut.stringValue1().c_str(), StrEq("flatterdude")); + EXPECT_THAT(sut.stringValue2().c_str(), StrEq("evilhuhn")); + EXPECT_THAT(sut.optionalInt1(), Eq(4711123)); + EXPECT_THAT(sut.optionalInt2(), Eq(810456)); + EXPECT_THAT(sut.optionalUint1(), Eq(39)); + EXPECT_THAT(sut.optionalUint2(), Eq(31)); + EXPECT_THAT(sut.lightSwitch1(), Eq(true)); + EXPECT_THAT(sut.lightSwitch2(), Eq(true)); + + EXPECT_THAT(sut.requiredString().c_str(), StrEq("schnappidububa")); + EXPECT_THAT(sut.requiredFloat(), Eq(456.123F)); + EXPECT_THAT(sut.requiredUint(), Eq(1212)); +} + +TEST_F(CommandLineArgumentDefinition_test, AllValuesViaCommandLineArgumentDefinitionAndShortcutAreSetCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "0c9abe4d-47ab-469a-a0fe-eff03a7aff37"); + CmdArgs args({"noOneBitesHypnotoad", + "-r", + "AllYouNeedIsHorst", + "-b", + "810.123", + "-c", + "31415", + "-s", + "DoNotTouchTheFishy", + "-t", + "NoLittleTouchyFishy", + "-i", + "3", + "-j", + "4", + "-u", + "5", + "-v", + "25", + "-l", + "-m"}); + auto sut = CommandLineArgumentDefinitionSut::parse(args.argc, args.argv, "My program description"); + + EXPECT_THAT(sut.binaryName(), StrEq("noOneBitesHypnotoad")); + + EXPECT_THAT(sut.stringValue1().c_str(), StrEq("DoNotTouchTheFishy")); + EXPECT_THAT(sut.stringValue2().c_str(), StrEq("NoLittleTouchyFishy")); + EXPECT_THAT(sut.optionalInt1(), Eq(3)); + EXPECT_THAT(sut.optionalInt2(), Eq(4)); + EXPECT_THAT(sut.optionalUint1(), Eq(5)); + EXPECT_THAT(sut.optionalUint2(), Eq(25)); + EXPECT_THAT(sut.lightSwitch1(), Eq(true)); + EXPECT_THAT(sut.lightSwitch2(), Eq(true)); + + EXPECT_THAT(sut.requiredString().c_str(), StrEq("AllYouNeedIsHorst")); + EXPECT_THAT(sut.requiredFloat(), Eq(810.123F)); + EXPECT_THAT(sut.requiredUint(), Eq(31415)); +} +} // namespace diff --git a/iceoryx_dust/test/moduletests/test_cli_command_line_common.hpp b/iceoryx_dust/test/moduletests/test_cli_command_line_common.hpp new file mode 100644 index 0000000000..544c222b6f --- /dev/null +++ b/iceoryx_dust/test/moduletests/test_cli_command_line_common.hpp @@ -0,0 +1,49 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef IOX_DUST_MODULETESTS_TEST_CXX_COMMAND_LINE_COMMON_HPP +#define IOX_DUST_MODULETESTS_TEST_CXX_COMMAND_LINE_COMMON_HPP + +#include +#include +#include + +struct CmdArgs +{ + int argc = 0; + char** argv = nullptr; + + explicit CmdArgs(const std::vector& arguments) + : argc{static_cast(arguments.size())} + , argv{new char*[static_cast(argc)]} + { + contents = std::make_unique>(arguments); + for (uint64_t i = 0; i < static_cast(argc); ++i) + { + argv[i] = const_cast((*contents)[i].data()); + } + } + + ~CmdArgs() + { + delete[] argv; + } + + std::unique_ptr> contents; +}; + + +#endif diff --git a/iceoryx_dust/test/moduletests/test_cli_command_line_parser.cpp b/iceoryx_dust/test/moduletests/test_cli_command_line_parser.cpp new file mode 100644 index 0000000000..884a07473a --- /dev/null +++ b/iceoryx_dust/test/moduletests/test_cli_command_line_parser.cpp @@ -0,0 +1,1244 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_dust/internal/cli/command_line_parser.hpp" +#include "iceoryx_hoofs/error_handling/error_handling.hpp" +#include "iceoryx_hoofs/testing/mocks/error_handler_mock.hpp" +#include "test.hpp" +#include "test_cli_command_line_common.hpp" + +#include +#include +#include +#include + +namespace +{ +using namespace ::testing; +using namespace iox::cli; +using namespace iox::cli::internal; +using namespace iox::cxx; + +class CommandLineParser_test : public Test +{ + public: + void SetUp() override + { + // if we do not capture stdout then the console is filled with garbage + // since the command line parser prints the help on failure + ::testing::internal::CaptureStdout(); + } + void TearDown() override + { + std::string output = ::testing::internal::GetCapturedStdout(); + if (Test::HasFailure()) + { + std::cout << output << std::endl; + } + } + + uint64_t numberOfErrorCallbackCalls = 0U; + iox::cxx::function errorCallback = [this] { ++numberOfErrorCallbackCalls; }; + static Argument_t defaultValue; +}; +Argument_t CommandLineParser_test::defaultValue = "DEFAULT VALUE"; + +TEST_F(CommandLineParser_test, SettingBinaryNameWorks) +{ + ::testing::Test::RecordProperty("TEST_ID", "bb5e0199-c061-4fa4-be14-c797f996fff6"); + const char* binaryName("AllHailHypnotoad"); + CmdArgs args({binaryName}); + auto options = parseCommandLineArguments(OptionDefinition(""), args.argc, args.argv); + + EXPECT_THAT(options.binaryName(), StrEq(binaryName)); +} + +TEST_F(CommandLineParser_test, EmptyArgcLeadsToExit) +{ + ::testing::Test::RecordProperty("TEST_ID", "627e7d26-7ba8-466f-8160-61dbff7f3a4d"); + IOX_DISCARD_RESULT(parseCommandLineArguments(OptionDefinition("", errorCallback), 0, nullptr)); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} + +void FailureTest(const std::vector& options, + const std::vector& optionsToRegister = {}, + const std::vector& switchesToRegister = {}, + const std::vector& requiredValuesToRegister = {}) noexcept +{ + const char* binaryName("GloryToTheHasselToad"); + std::vector optionVector{binaryName}; + optionVector.insert(optionVector.end(), options.begin(), options.end()); + CmdArgs args(optionVector); + + bool wasErrorHandlerCalled = false; + { + OptionDefinition optionSet("", [&] { wasErrorHandlerCalled = true; }); + for (const auto& o : optionsToRegister) + { + optionSet.addOptional(o[0], OptionName_t(TruncateToCapacity, o), "", "int", "0"); + } + for (const auto& s : switchesToRegister) + { + optionSet.addSwitch(s[0], OptionName_t{TruncateToCapacity, s}, ""); + } + for (const auto& r : requiredValuesToRegister) + { + optionSet.addRequired(r[0], OptionName_t(TruncateToCapacity, r), "", "int"); + } + + IOX_DISCARD_RESULT(parseCommandLineArguments(optionSet, args.argc, args.argv, 1U)); + } + + EXPECT_TRUE(wasErrorHandlerCalled); +} + +/// BEGIN syntax failure test + +TEST_F(CommandLineParser_test, FailSyntaxWhenOptionDoesNotStartWithDash_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "e463c987-a908-4cd5-b268-05a2cbda5be2"); + std::vector optionsToRegister{"i-have-no-dash"}; + FailureTest({"i-have-no-dash"}, optionsToRegister); + FailureTest({"i-have-no-dash", "someValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenOptionDoesNotStartWithDash_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "da57a066-83da-4bc0-994f-872d7713d8dd"); + std::vector optionsToRegister{"i-have-no-dash", "set", "bla"}; + // begin + FailureTest({"i-have-no-dash", "--set", "setValue", "--bla", "blaValue"}, optionsToRegister); + FailureTest({"i-have-no-dash", "someValue", "--set", "setValue", "--bla", "blaValue"}, optionsToRegister); + // middle + FailureTest({"--set", "setValue", "i-have-no-dash", "--bla", "blaValue"}, optionsToRegister); + FailureTest({"--set", "setValue", "i-have-no-dash", "someValue", "--bla", "blaValue"}, optionsToRegister); + // end + FailureTest({"--set", "setValue", "--bla", "blaValue", "i-have-no-dash"}, optionsToRegister); + FailureTest({"--set", "setValue", "--bla", "blaValue", "i-have-no-dash", "someValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenOptionDoesNotStartWithDash_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "b0f51f16-94ad-4a25-b6f7-11e7e2328472"); + std::vector optionsToRegister{"i-have-no-dash", "set", "bla"}; + // begin + FailureTest({"i", "-s", "setValue", "-b", "blaValue"}, optionsToRegister); + FailureTest({"i", "someValue", "-s", "setValue", "-b", "blaValue"}, optionsToRegister); + // middle + FailureTest({"-s", "setValue", "i", "-b", "blaValue"}, optionsToRegister); + FailureTest({"-s", "setValue", "i", "someValue", "-b", "blaValue"}, optionsToRegister); + // end + FailureTest({"-s", "setValue", "-b", "blaValue", "i"}, optionsToRegister); + FailureTest({"-s", "setValue", "-b", "blaValue", "i", "someValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenShortOptionNameIsEmpty_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "d85ef04b-d91e-438a-8804-bc21c1eebb84"); + FailureTest({"-"}); + FailureTest({"-", "someValue"}); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenShortOptionNameIsEmpty_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "39c9e200-dd22-4aef-a82e-af84f4336708"); + std::vector optionsToRegister{"set", "bla"}; + // begin + FailureTest({"-", "--set", "setValue123", "--bla", "blaValue455"}, optionsToRegister); + FailureTest({"-", "someValue", "--set", "setValue123", "--bla", "blaValue455"}, optionsToRegister); + // middle + FailureTest({"--set", "setValue123", "-", "--bla", "blaValue455"}, optionsToRegister); + FailureTest({"--set", "setValue123", "-", "someValue", "--bla", "blaValue455"}, optionsToRegister); + // end + FailureTest({"--set", "setValue123", "--bla", "blaValue455", "-"}, optionsToRegister); + FailureTest({"--set", "setValue123", "--bla", "blaValue455", "-", "someValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenOptionNameIsEmpty_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "5316ba5e-0490-4356-a81d-3afd89766b51"); + FailureTest({"--"}); + FailureTest({"--", "someValue"}); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenOptionNameIsEmpty_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "ca211062-8f23-49a2-8de7-9cffddae6a39"); + std::vector optionsToRegister{"set", "bla"}; + // begin + FailureTest({"--", "--bla", "blaValue123123", "--set", "setValueXXX"}, optionsToRegister); + FailureTest({"--", "someValue", "--bla", "blaValue123123", "--set", "setValueXXX"}, optionsToRegister); + // middle + FailureTest({"--bla", "blaValue123123", "--", "--set", "setValueXXX"}, optionsToRegister); + FailureTest({"--bla", "blaValue123123", "--", "someValue", "--set", "setValueXXX"}, optionsToRegister); + // end + FailureTest({"--bla", "blaValue123123", "--set", "setValueXXX", "--"}, optionsToRegister); + FailureTest({"--bla", "blaValue123123", "--set", "setValueXXX", "--", "someValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenShortOptionNameHasMoreThenOneLetter_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "13776543-6126-403c-96ea-9137590e9e74"); + std::vector optionsToRegister{"invalid-option"}; + FailureTest({"-invalid-option"}, optionsToRegister); + FailureTest({"-invalid-option", "someValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenShortOptionNameHasMoreThenOneLetter_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "3f4e337d-5e01-418a-8e30-1a947116ff53"); + std::vector optionsToRegister{"set", "bla", "invalid-option"}; + // begin + FailureTest({"-invalid-option", "--bla", "blaValue123123", "--set", "setValueXXX"}, optionsToRegister); + FailureTest({"-invalid-option", "someValue", "--bla", "blaValue123123", "--set", "setValueXXX"}, optionsToRegister); + // middle + FailureTest({"--bla", "blaValue123123", "-invalid-option", "--set", "setValueXXX"}, optionsToRegister); + FailureTest({"--bla", "blaValue123123", "-invalid-option", "someValue", "--set", "setValueXXX"}, optionsToRegister); + // end + FailureTest({"--bla", "blaValue123123", "--set", "setValueXXX", "-invalid-option"}, optionsToRegister); + FailureTest({"--bla", "blaValue123123", "--set", "setValueXXX", "-invalid-option", "someValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenLongOptionStartsWithTripleDash_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "39eff747-a03f-4c4c-bee3-bb970e32f5b5"); + std::vector optionsToRegister{"invalid-long-option"}; + FailureTest({"---invalid-long-option"}, optionsToRegister); + FailureTest({"---invalid-long-option", "someValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenLongOptionStartsWithTripleDash_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "f8b9f82a-a0f4-48c3-b88c-d9b997359d45"); + std::vector optionsToRegister{"set", "bla", "invalid-long-option"}; + // begin + FailureTest({"---invalid-long-option", "--bla", "blaValue123123", "--set", "setValueXXX"}, optionsToRegister); + FailureTest({"---invalid-long-option", "someValue", "--bla", "blaValue123123", "--set", "setValueXXX"}, + optionsToRegister); + // middle + FailureTest({"--bla", "blaValue123123", "---invalid-long-option", "--set", "setValueXXX"}, optionsToRegister); + FailureTest({"--bla", "blaValue123123", "---invalid-long-option", "someValue", "--set", "setValueXXX"}, + optionsToRegister); + // end + FailureTest({"--bla", "blaValue123123", "--set", "setValueXXX", "---invalid-long-option"}, optionsToRegister); + FailureTest({"--bla", "blaValue123123", "--set", "setValueXXX", "---invalid-long-option", "someValue"}, + optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenOptionNameExceedMaximumSize_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "8066d89f-0fc0-4db2-8bb5-11708f82794f"); + FailureTest({std::string("--") + std::string(MAX_OPTION_NAME_LENGTH + 1, 'a')}); + FailureTest({std::string("--") + std::string(MAX_OPTION_NAME_LENGTH + 1, 'a'), "someValue"}); +} + +TEST_F(CommandLineParser_test, FailSyntaxWhenOptionNameExceedMaximumSize_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "4c530a35-de80-4352-ae13-763a1ccfae5c"); + std::vector optionsToRegister{"set", "bla"}; + // begin + FailureTest( + {std::string("--") + std::string(MAX_OPTION_NAME_LENGTH + 1, 'a'), "--set", "setValue", "--bla", "blaValue"}, + optionsToRegister); + FailureTest({std::string("--") + std::string(MAX_OPTION_NAME_LENGTH + 1, 'a'), + "someValue", + "--set", + "setValue", + "--bla", + "blaValue"}, + optionsToRegister); + // middle + FailureTest( + {"--set", "setValue", std::string("--") + std::string(MAX_OPTION_NAME_LENGTH + 1, 'a'), "--bla", "blaValue"}, + optionsToRegister); + FailureTest({"someValue", + "--set", + std::string("--") + std::string(MAX_OPTION_NAME_LENGTH + 1, 'a'), + "setValue", + "--bla", + "blaValue"}, + optionsToRegister); + // end + FailureTest( + {"--set", "setValue", "--bla", "blaValue", std::string("--") + std::string(MAX_OPTION_NAME_LENGTH + 1, 'a')}, + optionsToRegister); + FailureTest({"--set", + "setValue", + "--bla", + "blaValue", + std::string("--") + std::string(MAX_OPTION_NAME_LENGTH + 1, 'a'), + "someValue"}, + optionsToRegister); +} + +/// END syntax failure test + +/// BEGIN option failure test +TEST_F(CommandLineParser_test, FailWhenOptionWasNotRegistered_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "ce0c8994-7999-41cf-8356-dafc6cfd5107"); + std::vector optionsToRegister{"sputnik", "rosetta"}; + FailureTest({"--conway", "gameOfLife"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenOptionWasNotRegistered_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "68e4cdb8-b50d-42da-a51a-2c98b882613b"); + std::vector optionsToRegister{"sputnik", "rosetta"}; + // begin + FailureTest({"--conway", "gameOfLife", "--sputnik", "iWasFirst", "--rosetta", "uhWhatsThere"}, optionsToRegister); + // middle + FailureTest({"--sputnik", "iWasFirst", "--conway", "gameOfLife", "--rosetta", "uhWhatsThere"}, optionsToRegister); + // end + FailureTest({"--sputnik", "iWasFirst", "--rosetta", "uhWhatsThere", "--conway", "gameOfLife"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenOptionWasNotRegistered_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "a4de02ed-3057-4d54-bcad-5a55ed8ea9ed"); + std::vector optionsToRegister{"sputnik", "rosetta"}; + // begin + FailureTest({"-c", + "gameOfLife", + "-s", + "iWasFirst" + "-r", + "uhWhatsThere"}, + optionsToRegister); + // middle + FailureTest({"-s", "gameOfLife", "-c", "gameOfLife", "-r", "uhWhatsThere"}, optionsToRegister); + // end + FailureTest({"-s", "gameOfLife", "-r", "uhWhatsThere", "-c", "gameOfLife"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenValueOptionIsFollowedByAnotherOption_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "72eb64a6-a323-4755-a7d4-303a04b31383"); + std::vector optionsToRegister{"set"}; + FailureTest({"--set"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenValueOptionIsFollowedByAnotherOption_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "0743ad80-d6dc-4095-b1c3-d81562eb4c85"); + std::vector optionsToRegister{"set", "bla", "fuu", "oh-no-i-am-an-option"}; + FailureTest({"--fuu", "fuuValue", "--bla", "blaValue", "--set", "someValue", "--oh-no-i-am-an-option"}, + optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenValueOptionIsFollowedByAnotherOption_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "0bc9bbf0-b1fe-4d59-89de-d3462d90f7ff"); + std::vector optionsToRegister{"set", "bla", "fuu", "oh-no-i-am-an-option"}; + FailureTest({"-f", "fuuValue", "-b", "blaValue", "-s", "blubb", "-o"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenValueOptionIsSetMultipleTimes_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "1c17a7f9-6a34-4bb4-a4f4-0415c92325d4"); + std::vector optionsToRegister{"set"}; + FailureTest({"--set", "bla", "--set", "fuu"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenValueOptionIsSetMultipleTimes_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "2c1f9325-4839-41eb-a0ea-8a8ca06e5357"); + std::vector optionsToRegister{"set", "bla", "fuu"}; + FailureTest({"--set", "fuuu", "--bla", "blaValue", "--fuu", "fuuValue", "--set", "bla"}, optionsToRegister); + FailureTest({"--bla", "blaValue", "--set", "fuuu", "--fuu", "fuuValue", "--set", "bla"}, optionsToRegister); + FailureTest({"--set", "fuuu", "--bla", "blaValue", "--set", "bla", "--fuu", "fuuValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenValueOptionIsSetMultipleTimes_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "17fd7b1c-6026-4b15-b0c2-862c4ce0b00b"); + std::vector optionsToRegister{"set", "bla", "fuu"}; + FailureTest({"-s", "fuuu", "-b", "blaValue", "-f", "fuuValue", "-s", "bla"}, optionsToRegister); + FailureTest({"-b", "blaValue", "-s", "fuuu", "-f", "fuuValue", "-s", "bla"}, optionsToRegister); + FailureTest({"-s", "fuuu", "-b", "blaValue", "-s", "bla", "-f", "fuuValue"}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenOptionValueExceedMaximumSize_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "500e56b2-b7a6-4ed9-8583-d109f530d09f"); + std::vector optionsToRegister{"set"}; + FailureTest({"--set", std::string(MAX_OPTION_ARGUMENT_LENGTH + 1, 'a')}, optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenOptionValueExceedMaximumSize_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "04b6ef11-8882-4e6e-8759-44eac330c822"); + std::vector optionsToRegister{"set", "bla", "fuu"}; + + // begin + FailureTest({"--set", std::string(MAX_OPTION_ARGUMENT_LENGTH + 1, 'a'), "--bla", "blaValue", "--fuu", "fuuValue"}, + optionsToRegister); + // middle + FailureTest({"--set", "blaValue", "--bla", std::string(MAX_OPTION_ARGUMENT_LENGTH + 1, 'a'), "--fuu", "fuuValue"}, + optionsToRegister); + // end + FailureTest({"--set", "blaValue", "--bla", "fuuValue", "--fuu", std::string(MAX_OPTION_ARGUMENT_LENGTH + 1, 'a')}, + optionsToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenOptionValueExceedMaximumSize_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "4a74242a-d7c5-4275-8a7b-2249107036ff"); + std::vector optionsToRegister{"set", "bla", "fuu"}; + + // begin + FailureTest({"-s", std::string(MAX_OPTION_ARGUMENT_LENGTH + 1, 'a'), "-b", "blaValue", "-f", "fuuValue"}, + optionsToRegister); + // middle + FailureTest({"-s", "blaValue", "-b", std::string(MAX_OPTION_ARGUMENT_LENGTH + 1, 'a'), "-f", "fuuValue"}, + optionsToRegister); + // end + FailureTest({"-s", "blaValue", "-b", "fuuValue", "-f", std::string(MAX_OPTION_ARGUMENT_LENGTH + 1, 'a')}, + optionsToRegister); +} +/// END option failure test + +/// BEGIN switch failure test +TEST_F(CommandLineParser_test, FailWhenSwitchWasNotRegistered_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "92236356-4729-414e-8edc-65eb23cd20d0"); + std::vector optionsToRegister; + std::vector switchesToRegister{"supergandalf", "grand-alf"}; + + FailureTest({"--mario"}, optionsToRegister, switchesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenSwitchWasNotRegistered_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "f5a9fa53-4170-4f21-a028-f4cfd7beeac3"); + std::vector optionsToRegister; + std::vector switchesToRegister{"supergandalf", "grand-alf"}; + + // begin + FailureTest({"--mario", "--supergandalf", "--grand-alf"}, optionsToRegister, switchesToRegister); + // middle + FailureTest({"--supergandalf", "--mario", "--grand-alf"}, optionsToRegister, switchesToRegister); + // end + FailureTest({"--supergandalf", "--grand-alf", "--mario"}, optionsToRegister, switchesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenSwitchWasNotRegistered_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "311873e0-159f-4f0a-8228-32e659bd52ea"); + std::vector optionsToRegister; + std::vector switchesToRegister{"supergandalf", "grand-alf"}; + + // begin + FailureTest({"-m", "-s", "-g"}, optionsToRegister, switchesToRegister); + // middle + FailureTest({"-s", "-m", "-g"}, optionsToRegister, switchesToRegister); + // end + FailureTest({"-s", "-g", "-m"}, optionsToRegister, switchesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenSwitchHasValueSet_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "24d76c82-dc7b-48b3-a88b-dada402802cc"); + std::vector optionsToRegister; + std::vector switchesToRegister{"set"}; + + FailureTest({"--set", "noValueAfterSwitch"}, optionsToRegister, switchesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenSwitchHasValueSet_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "b8b562ad-d502-4ada-8eac-8d8ffce22689"); + std::vector optionsToRegister; + std::vector switchesToRegister{"set", "bla", "fuu"}; + + // begin + FailureTest({"--set", "noValueAfterSwitch", "--bla", "--fuu"}, optionsToRegister, switchesToRegister); + // middle + FailureTest({"--set", "--bla", "noValueAfterSwitch", "--fuu"}, optionsToRegister, switchesToRegister); + // end + FailureTest({"--set", "--bla", "--fuu", "noValueAfterSwitch"}, optionsToRegister, switchesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenSwitchHasValueSet_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "e03c4823-00e2-4d4d-acf0-933086e77bef"); + std::vector optionsToRegister; + std::vector switchesToRegister{"set", "bla", "fuu"}; + + // begin + FailureTest({"-s", "noValueAfterSwitch", "-b", "-f"}, optionsToRegister, switchesToRegister); + // middle + FailureTest({"-s", "-b", "noValueAfterSwitch", "-f"}, optionsToRegister, switchesToRegister); + // end + FailureTest({"-s", "-b", "-f", "noValueAfterSwitch"}, optionsToRegister, switchesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenSwitchIsSetMultipleTimes_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "a900e3c2-c3ef-415e-b8ce-734ce44c479e"); + std::vector optionsToRegister; + std::vector switchesToRegister{"set"}; + FailureTest({"--set", "--set"}, optionsToRegister, switchesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenSwitchIsSetMultipleTimes_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "5010cb12-cd30-470d-8065-2618d3b257c3"); + std::vector optionsToRegister; + std::vector switchesToRegister{"set", "bla", "fuu"}; + + // begin + FailureTest({"--set", "--set", "--bla", "--fuu"}, optionsToRegister, switchesToRegister); + // middle + FailureTest({"--set", "--bla", "--set", "--fuu"}, optionsToRegister, switchesToRegister); + // end + FailureTest({"--set", "--bla", "--fuu", "--set"}, optionsToRegister, switchesToRegister); + // center + FailureTest({"--set", "--fuu", "--fuu", "--bla"}, optionsToRegister, switchesToRegister); +} + +/// END switch failure test + +/// BEGIN required option failure test +TEST_F(CommandLineParser_test, FailWhenRequiredOptionIsNotPresent_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "1007661d-4d84-49a1-8554-9f21f2ccc3f3"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"set", "fuu"}; + + FailureTest({"--set", "ohIForgotFuu"}, optionsToRegister, switchesToRegister, requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenRequiredOptionIsNotPresent_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "4786109a-f7b2-42c9-a40b-d1fb94a45432"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"set", "fuu", "bla", "muu"}; + + // begin + FailureTest({"--bla", "ohIForgotSet", "--fuu", "someFuu", "--muu", "blaarb"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + // middle + FailureTest({"--set", "ohIForgetBla", "--fuu", "someFuu", "--muu", "blaarb"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + // end + FailureTest({"--set", "ohIForgotMuu", "--fuu", "someFuu", "--bla", "someBlaa"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenRequiredOptionIsNotPresent_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "cd7f22f2-cde4-48e3-8c0a-5610fee3c9fc"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"set", "fuu", "bla", "muu"}; + + // begin + FailureTest({"-b", "ohIForgotSet", "-f", "someFuu", "-m", "blaarb"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + // middle + FailureTest({"-s", "ohIForgetBla", "-f", "someFuu", "-m", "blaarb"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + // end + FailureTest({"-s", "ohIForgotMuu", "-f", "someFuu", "-b", "someBlaa"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenRequiredOptionIsNotFollowedByValue_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "154306d7-816c-4200-a1ad-f27a3cdb62e1"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"set"}; + + FailureTest({"--set"}, optionsToRegister, switchesToRegister, requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenRequiredOptionIsNotFollowedByValue_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "e97c1d07-39e8-48e8-b941-d0b6f3f7e73f"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"set", "fuu", "bla", "toad"}; + + // begin + FailureTest({"--set", "--fuu", "someValue", "--bla", "blaValue", "--toad", "hypno"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + // middle + FailureTest({"--set", "someSet", "--fuu", "someValue", "--bla", "--toad", "hypno"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + // end + FailureTest({"--set", "someSet", "--fuu", "someValue", "--bla", "--toad"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenRequiredOptionIsNotFollowedByValue_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "8afa4f3a-5d94-4cf4-aa10-b3612ecfc817"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"set", "fuu", "bla", "toad"}; + + // begin + FailureTest({"-s", "-f", "someValue", "-b", "blaValue", "-t", "hypno"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + // middle + FailureTest({"-s", "someSet", "-f", "someValue", "-b", "-t", "hypno"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + // end + FailureTest({"-s", "someSet", "-f", "someValue", "-b", "-t"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} +/// END required option failure test + +/// BEGIN required, optional option and switch failure mix +TEST_F(CommandLineParser_test, FailWhenOneRequiredOptionIsNotSet_MixedArguments) +{ + ::testing::Test::RecordProperty("TEST_ID", "c437b65b-585b-4ec9-8a3a-abb7add92f0c"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"i-req", "j-req", "k-req"}; + + FailureTest({"--d-switch", "--f-switch", "--a-opt", "someA", "--k-req", "fSet", "--i-req", "asd"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenMultipleRequiredOptionsAreNotSet_MixedArguments) +{ + ::testing::Test::RecordProperty("TEST_ID", "e36058bb-2bde-4781-9aff-8e9fa524e925"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"i-req", "j-req", "k-req"}; + + FailureTest({"--d-switch", "--f-switch", "--a-opt", "someA", "--i-req", "asd", "--b-opt", "asd"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenNoRequiredOptionIsSet_MixedArguments) +{ + ::testing::Test::RecordProperty("TEST_ID", "a8e079aa-6d35-4ea6-9df0-f9d14ecab0ec"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"i-req", "j-req", "k-req"}; + + FailureTest({"--d-switch", "--f-switch", "--a-opt", "someA", "--e-switch", "--b-opt", "asd"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenSwitchHasValueSet_MixedArguments) +{ + ::testing::Test::RecordProperty("TEST_ID", "9b1d7663-2c26-417c-a5d3-d9d4b67e104d"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"i-req", "j-req", "k-req"}; + + FailureTest({"--d-switch", + "ohNoASwitchValue", + "--f-switch", + "--a-opt", + "someA", + "--k-req", + "fSet", + "--i-req", + "asd", + "--j-req", + "fuu"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenOptionHasNoValueSet_MixedArguments) +{ + ::testing::Test::RecordProperty("TEST_ID", "e90be9ac-8839-4252-84b3-e487ceb095d0"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"i-req", "j-req", "k-req"}; + + FailureTest({"--d-switch", + "--f-switch", + "--a-opt", + "ohBHasNoValue", + "--b-opt", + "--k-req", + "fSet", + "--i-req", + "asd", + "--j-req", + "fuu"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenRequiredOptionHasNoValueSet_MixedArguments) +{ + ::testing::Test::RecordProperty("TEST_ID", "7d69a1a9-3235-42b4-a88f-dbfd2886d5ef"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"i-req", "j-req", "k-req"}; + + FailureTest({"--d-switch", + "--f-switch", + "--a-opt", + "aVal", + "--b-opt", + "bVal", + "--k-req", + "ohNoIHasNoValue", + "--i-req", + "--j-req", + "fuu"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenOptionIsNotRegistered_MixedArguments) +{ + ::testing::Test::RecordProperty("TEST_ID", "f7c314e7-f103-45be-b5f8-f96e01a2e3cc"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"i-req", "j-req", "k-req"}; + + FailureTest({"--d-switch", + "--f-switch", + "--a-opt", + "aVal", + "--nobody-knows-me", + "mrUnknown", + "--b-opt", + "bVal", + "--k-req", + "ohNoIHasNoValue", + "--i-req", + "someI", + "--j-req", + "fuu"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} + +TEST_F(CommandLineParser_test, FailWhenSwitchIsNotRegistered_MixedArguments) +{ + ::testing::Test::RecordProperty("TEST_ID", "893857b5-e3f5-4a4d-8da1-ed52ff15ef33"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"i-req", "j-req", "k-req"}; + + FailureTest({"--unknown-switch", + "--d-switch", + "--f-switch", + "--a-opt", + "aVal", + "--b-opt", + "bVal", + "--k-req", + "ohNoIHasNoValue", + "--i-req", + "someI", + "--j-req", + "fuu"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); +} +/// END required, optional option and switch failure mix + +TEST_F(CommandLineParser_test, DefaultValuesAreLoadedForShortOptionsOnly) +{ + ::testing::Test::RecordProperty("TEST_ID", "b3b71f74-29d6-44b2-b1b7-c92a99e3ba5c"); + + constexpr int32_t DEFAULT_VALUE_1 = 4712; + constexpr int32_t DEFAULT_VALUE_2 = 19230; + + OptionDefinition optionSet(""); + optionSet.addOptional('a', "", "", "int", Argument_t(TruncateToCapacity, std::to_string(DEFAULT_VALUE_1).c_str())); + optionSet.addOptional('b', "", "", "int", Argument_t(TruncateToCapacity, std::to_string(DEFAULT_VALUE_2).c_str())); + + CmdArgs args({"binaryName"}); + auto retVal = parseCommandLineArguments(optionSet, args.argc, args.argv, 0); + + auto result1 = retVal.get("a"); + ASSERT_FALSE(result1.has_error()); + EXPECT_THAT(*result1, Eq(DEFAULT_VALUE_1)); + + auto result2 = retVal.get("b"); + ASSERT_FALSE(result2.has_error()); + EXPECT_THAT(*result2, Eq(DEFAULT_VALUE_2)); +} + +TEST_F(CommandLineParser_test, DefaultValuesAreLoadedForLongOptionsOnly) +{ + ::testing::Test::RecordProperty("TEST_ID", "d3efb8c4-b546-418e-85b9-a6c2b447c0cb"); + + constexpr int32_t DEFAULT_VALUE_1 = 187293; + constexpr int32_t DEFAULT_VALUE_2 = 5512341; + + OptionDefinition optionSet(""); + optionSet.addOptional(iox::cli::NO_SHORT_OPTION, + "bla", + "", + "int", + Argument_t(TruncateToCapacity, std::to_string(DEFAULT_VALUE_1).c_str())); + optionSet.addOptional(iox::cli::NO_SHORT_OPTION, + "fuu", + "", + "int", + Argument_t(TruncateToCapacity, std::to_string(DEFAULT_VALUE_2).c_str())); + + CmdArgs args({"binaryName"}); + auto retVal = parseCommandLineArguments(optionSet, args.argc, args.argv, 0); + + auto result1 = retVal.get("bla"); + ASSERT_FALSE(result1.has_error()); + EXPECT_THAT(*result1, Eq(DEFAULT_VALUE_1)); + + auto result2 = retVal.get("fuu"); + ASSERT_FALSE(result2.has_error()); + EXPECT_THAT(*result2, Eq(DEFAULT_VALUE_2)); +} + +TEST_F(CommandLineParser_test, DetectMissingRequiredOptionsWithShortOptionsOnly) +{ + ::testing::Test::RecordProperty("TEST_ID", "a414b0f8-88cc-4a0a-bc73-c9be1f5dcc79"); + bool wasErrorHandlerCalled; + OptionDefinition optionSet("", [&] { wasErrorHandlerCalled = true; }); + optionSet.addRequired('a', "", "", "int"); + optionSet.addRequired('b', "", "", "int"); + + CmdArgs args({"binaryName"}); + auto retVal = parseCommandLineArguments(optionSet, args.argc, args.argv, 0); + + EXPECT_THAT(wasErrorHandlerCalled, Eq(true)); +} + +TEST_F(CommandLineParser_test, DetectMissingRequiredOptionsWithLongOptionsOnly) +{ + ::testing::Test::RecordProperty("TEST_ID", "8115efb2-9ddf-4457-9d69-526f215d974a"); + bool wasErrorHandlerCalled; + OptionDefinition optionSet("", [&] { wasErrorHandlerCalled = true; }); + optionSet.addRequired(iox::cli::NO_SHORT_OPTION, "alpha", "", "int"); + optionSet.addRequired(iox::cli::NO_SHORT_OPTION, "beta", "", "int"); + + CmdArgs args({"binaryName"}); + auto retVal = parseCommandLineArguments(optionSet, args.argc, args.argv, 0); + + EXPECT_THAT(wasErrorHandlerCalled, Eq(true)); +} + +Arguments SuccessTest(const std::vector& options, + const std::vector& optionsToRegister = {}, + const std::vector& switchesToRegister = {}, + const std::vector& requiredValuesToRegister = {}, + const uint64_t argcOffset = 1U) noexcept +{ + const char* binaryName("GloryToTheHasselToad"); + std::vector optionVector{binaryName}; + optionVector.insert(optionVector.end(), options.begin(), options.end()); + CmdArgs args(optionVector); + Arguments retVal; + + bool wasErrorHandlerCalled = false; + { + OptionDefinition optionSet(""); + for (const auto& o : optionsToRegister) + { + optionSet.addOptional( + o[0], OptionName_t(TruncateToCapacity, o), "", "int", CommandLineParser_test::defaultValue); + } + for (const auto& s : switchesToRegister) + { + optionSet.addSwitch(s[0], OptionName_t{TruncateToCapacity, s}, ""); + } + for (const auto& r : requiredValuesToRegister) + { + optionSet.addRequired(r[0], OptionName_t(TruncateToCapacity, r), "", "int"); + } + + { + auto handle = iox::ErrorHandlerMock::setTemporaryErrorHandler( + [&](const iox::HoofsError, const iox::ErrorLevel) { wasErrorHandlerCalled = true; }); + retVal = parseCommandLineArguments(optionSet, args.argc, args.argv, argcOffset); + } + } + EXPECT_FALSE(wasErrorHandlerCalled); + return retVal; +} + +template +void verifyEntry(const Arguments& options, const OptionName_t& entry, const iox::cxx::optional& value) +{ + auto result = options.get(entry); + + value + .and_then([&](auto& v) { + // ASSERT_ does not work in function calls + if (result.has_error()) + { + EXPECT_TRUE(false); + return; + } + + EXPECT_THAT(*result, Eq(v)); + }) + .or_else([&] { + if (!result.has_error()) + { + EXPECT_TRUE(false); + return; + } + + EXPECT_THAT(result.get_error(), Eq(Arguments::Error::UNABLE_TO_CONVERT_VALUE)); + }); +} + +/// BEGIN acquire values correctly + +TEST_F(CommandLineParser_test, ReadOptionSuccessfully_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "b5e7b7b0-9423-4ea7-a1c5-83c891fc39fd"); + std::vector optionsToRegister{"conway"}; + auto option = SuccessTest({"--conway", "gameOfLife"}, optionsToRegister); + + verifyEntry(option, "conway", {"gameOfLife"}); +} + +TEST_F(CommandLineParser_test, ReadOptionSuccessfully_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "68c91bc7-b56d-4fdd-a835-32173fe7e05c"); + std::vector optionsToRegister{"conway", "tungsten", "moon"}; + auto option = SuccessTest({"--moon", "bright", "--conway", "gameOfLife", "--tungsten", "heavy"}, optionsToRegister); + + verifyEntry(option, "conway", {"gameOfLife"}); + verifyEntry(option, "moon", {"bright"}); + verifyEntry(option, "tungsten", {"heavy"}); +} + +TEST_F(CommandLineParser_test, ReadOptionSuccessfully_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "6113b30a-1274-4b2b-b6e2-45cfad493b45"); + std::vector optionsToRegister{"conway", "tungsten", "moon"}; + auto option = SuccessTest({"-m", "bright", "-c", "gameOfLife", "-t", "heavy"}, optionsToRegister); + + verifyEntry(option, "c", {"gameOfLife"}); + verifyEntry(option, "m", {"bright"}); + verifyEntry(option, "t", {"heavy"}); +} + +TEST_F(CommandLineParser_test, ReadOptionSuccessfully_PartialSet) +{ + ::testing::Test::RecordProperty("TEST_ID", "7432b080-6d18-424b-bbe1-5c9293e8584a"); + std::vector optionsToRegister{"conway", "tungsten", "moon"}; + auto option = SuccessTest({"-m", "bright"}, optionsToRegister); + + verifyEntry(option, "moon", {"bright"}); + verifyEntry(option, "conway", {defaultValue.c_str()}); + verifyEntry(option, "tungsten", {defaultValue.c_str()}); +} + +TEST_F(CommandLineParser_test, ReadOptionSuccessfully_Offset) +{ + ::testing::Test::RecordProperty("TEST_ID", "58a5f953-f33f-48a6-ac48-d60006726cb6"); + std::vector optionsToRegister{"conway", "tungsten", "moon"}; + constexpr uint64_t ARGC_OFFSET = 5U; + auto option = + SuccessTest({"whatever", "bright", "-t", "heavy", "-c", "gameOfLife"}, optionsToRegister, {}, {}, ARGC_OFFSET); + + verifyEntry(option, "moon", {defaultValue.c_str()}); + verifyEntry(option, "conway", {"gameOfLife"}); + verifyEntry(option, "tungsten", {defaultValue.c_str()}); +} + +TEST_F(CommandLineParser_test, ReadRequiredValueSuccessfully_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "8397de7f-5a1b-49d7-80bd-30a28f143efa"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"fuubar"}; + auto option = SuccessTest({"--fuubar", "ohFuBa"}, optionsToRegister, switchesToRegister, requiredValuesToRegister); + + verifyEntry(option, "fuubar", {"ohFuBa"}); +} + +TEST_F(CommandLineParser_test, ReadRequiredValueSuccessfully_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "fa54f70f-be0d-426a-a00b-4d4d3a57a90d"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"fuubar", "c64", "amiga"}; + auto option = SuccessTest({"--fuubar", "ohFuBa", "--amiga", "Os2 Warp", "--c64", "cobra"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + + verifyEntry(option, "fuubar", {"ohFuBa"}); + verifyEntry(option, "amiga", {"Os2 Warp"}); + verifyEntry(option, "c64", {"cobra"}); +} + +TEST_F(CommandLineParser_test, ReadRequiredValueSuccessfully_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "aeefeddf-7578-4451-a266-116219fdb150"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"fuubar", "c64", "amiga"}; + auto option = SuccessTest({"-f", "ohFuBa", "-a", "Os2 Warp", "-c", "cobra"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + + verifyEntry(option, "f", {"ohFuBa"}); + verifyEntry(option, "a", {"Os2 Warp"}); + verifyEntry(option, "c", {"cobra"}); +} + +TEST_F(CommandLineParser_test, ReadRequiredValueSuccessfully_Offset) +{ + ::testing::Test::RecordProperty("TEST_ID", "f84a9ad7-c6d9-4b56-bd18-a0d1bdbcde2a"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"fuubar", "c64", "amiga"}; + constexpr uint64_t ARGC_OFFSET = 3U; + auto option = SuccessTest({"-f", "iWillNotBeParsed", "-f", "ohFuBa", "-a", "Os2 Warp", "-c", "cobra"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister, + ARGC_OFFSET); + + verifyEntry(option, "f", {"ohFuBa"}); + verifyEntry(option, "a", {"Os2 Warp"}); + verifyEntry(option, "c", {"cobra"}); +} + +TEST_F(CommandLineParser_test, ReadSwitchValueSuccessfullyWhenSet_SingleArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "08cfe8c0-6b37-418b-9bb0-265ba4419513"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{"light"}; + auto option = SuccessTest({"--light"}, optionsToRegister, switchesToRegister); + + EXPECT_TRUE(option.isSwitchSet("light")); +} + +TEST_F(CommandLineParser_test, ReadSwitchValueSuccessfullyWhenSet_MultiArgument) +{ + ::testing::Test::RecordProperty("TEST_ID", "1bdba94b-1f42-4627-b847-16a2b49c08b0"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{"light", "fridge", "muu"}; + auto option = SuccessTest({"--light", "--fridge", "--muu"}, optionsToRegister, switchesToRegister); + + EXPECT_TRUE(option.isSwitchSet("light")); + EXPECT_TRUE(option.isSwitchSet("fridge")); + EXPECT_TRUE(option.isSwitchSet("muu")); +} + +TEST_F(CommandLineParser_test, ReadSwitchValueSuccessfullyWhenSet_MultiArgument_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "85a0e72c-3eb0-416d-8e4f-7b49982ecaf8"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{"light", "fridge", "muu"}; + auto option = SuccessTest({"-l", "-f", "-m"}, optionsToRegister, switchesToRegister); + + EXPECT_TRUE(option.isSwitchSet("l")); + EXPECT_TRUE(option.isSwitchSet("f")); + EXPECT_TRUE(option.isSwitchSet("m")); +} + +TEST_F(CommandLineParser_test, ReadSwitchValueSuccessfullyWhenSet_PartialSet) +{ + ::testing::Test::RecordProperty("TEST_ID", "9f203ddd-2505-40b4-84b1-2246c4e7cf3a"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{"light", "fridge", "muu"}; + auto option = SuccessTest({"-l"}, optionsToRegister, switchesToRegister); + + EXPECT_TRUE(option.isSwitchSet("light")); + EXPECT_FALSE(option.isSwitchSet("fridge")); + EXPECT_FALSE(option.isSwitchSet("muu")); +} + +TEST_F(CommandLineParser_test, ReadSwitchValueSuccessfullyWhenSet_Offset) +{ + ::testing::Test::RecordProperty("TEST_ID", "e90f18dc-32c7-4b39-adb7-17e039a165d8"); + std::vector optionsToRegister{}; + std::vector switchesToRegister{"light", "fridge", "muu"}; + constexpr uint64_t ARGC_OFFSET = 2U; + auto option = + SuccessTest({"----unknown-dont-care", "-f", "-m"}, optionsToRegister, switchesToRegister, {}, ARGC_OFFSET); + + EXPECT_FALSE(option.isSwitchSet("light")); + EXPECT_TRUE(option.isSwitchSet("fridge")); + EXPECT_TRUE(option.isSwitchSet("muu")); +} +/// END acquire values correctly + +/// BEGIN acquire mixed values correctly + +TEST_F(CommandLineParser_test, ReadMixedValueSuccessfully) +{ + ::testing::Test::RecordProperty("TEST_ID", "eb1c565d-a10b-4a80-b6e4-1aac54b96324"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"g-req", "i-req", "j-req"}; + auto option = SuccessTest({"--a-opt", + "oh-my-blah", + "--d-switch", + "--i-req", + "someI", + "--j-req", + "someJ", + "--f-switch", + "--g-req", + "someG"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + + verifyEntry(option, "a-opt", {"oh-my-blah"}); + verifyEntry(option, "b-opt", {defaultValue.c_str()}); + verifyEntry(option, "c-opt", {defaultValue.c_str()}); + verifyEntry(option, "i-req", {"someI"}); + verifyEntry(option, "j-req", {"someJ"}); + verifyEntry(option, "g-req", {"someG"}); + + EXPECT_TRUE(option.isSwitchSet("d-switch")); + EXPECT_FALSE(option.isSwitchSet("e-switch")); + EXPECT_TRUE(option.isSwitchSet("f-switch")); +} + +TEST_F(CommandLineParser_test, ReadMixedValueSuccessfully_ShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "a250997f-7bc8-4ba6-a860-b8d650f59f39"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"g-req", "i-req", "j-req"}; + auto option = SuccessTest({"-a", "anotherA", "-b", "someB", "-e", "-i", "blaI", "-j", "blaJ", "-g", "blaG"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + + verifyEntry(option, "a-opt", {"anotherA"}); + verifyEntry(option, "b-opt", {"someB"}); + verifyEntry(option, "c-opt", {defaultValue.c_str()}); + verifyEntry(option, "i-req", {"blaI"}); + verifyEntry(option, "j-req", {"blaJ"}); + verifyEntry(option, "g-req", {"blaG"}); + + EXPECT_FALSE(option.isSwitchSet("d-switch")); + EXPECT_TRUE(option.isSwitchSet("e-switch")); + EXPECT_FALSE(option.isSwitchSet("f-switch")); +} + +TEST_F(CommandLineParser_test, ReadMixedValueSuccessfully_Offset) +{ + ::testing::Test::RecordProperty("TEST_ID", "3bb7943e-d446-4f83-8a6a-5cc9de997359"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{"d-switch", "e-switch", "f-switch"}; + std::vector requiredValuesToRegister{"g-req", "i-req", "j-req"}; + constexpr uint64_t ARGC_OFFSET = 3U; + auto option = SuccessTest({"-a", "anotherA", "-b", "someB", "-e", "-i", "blaI", "-j", "blaJ", "-g", "blaG"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister, + ARGC_OFFSET); + + verifyEntry(option, "a-opt", {defaultValue.c_str()}); + verifyEntry(option, "b-opt", {"someB"}); + verifyEntry(option, "c-opt", {defaultValue.c_str()}); + verifyEntry(option, "i-req", {"blaI"}); + verifyEntry(option, "j-req", {"blaJ"}); + verifyEntry(option, "g-req", {"blaG"}); + + EXPECT_FALSE(option.isSwitchSet("d-switch")); + EXPECT_TRUE(option.isSwitchSet("e-switch")); + EXPECT_FALSE(option.isSwitchSet("f-switch")); +} +/// END acquire mixed values correctly + +/// BEGIN conversions +TEST_F(CommandLineParser_test, SuccessfulConversionToNumbers) +{ + ::testing::Test::RecordProperty("TEST_ID", "f3ffc60c-bcf8-4c4c-99c8-16f81625893f"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"g-req", "i-req", "j-req"}; + + auto option = SuccessTest({"--a-opt", "123", "--i-req", "-456", "--j-req", "123.123", "--g-req", "-891.19012"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + + verifyEntry(option, "a-opt", {123}); + verifyEntry(option, "i-req", {-456}); + verifyEntry(option, "j-req", {123.123f}); + verifyEntry(option, "g-req", {-891.19012}); +} + +TEST_F(CommandLineParser_test, MultipleConversionFailures) +{ + ::testing::Test::RecordProperty("TEST_ID", "0a4bc316-7a0a-4524-b3b4-1bcf30f87380"); + std::vector optionsToRegister{"a-opt", "b-opt", "c-opt"}; + std::vector switchesToRegister{}; + std::vector requiredValuesToRegister{"g-req", "i-req", "j-req"}; + + auto option = + SuccessTest({"--a-opt", "-123", "--i-req", "123123123", "--j-req", "iAmNotAFloat", "--g-req", "-891.19012"}, + optionsToRegister, + switchesToRegister, + requiredValuesToRegister); + + verifyEntry(option, "a-opt", iox::cxx::nullopt); + verifyEntry(option, "i-req", iox::cxx::nullopt); + verifyEntry(option, "j-req", iox::cxx::nullopt); + verifyEntry(option, "g-req", iox::cxx::nullopt); +} +/// END conversions + +} // namespace diff --git a/iceoryx_dust/test/moduletests/test_cli_option.cpp b/iceoryx_dust/test/moduletests/test_cli_option.cpp new file mode 100644 index 0000000000..c974d289d6 --- /dev/null +++ b/iceoryx_dust/test/moduletests/test_cli_option.cpp @@ -0,0 +1,326 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_dust/internal/cli/option.hpp" +#include "test.hpp" + + +namespace +{ +using namespace ::testing; +using namespace iox::cli; +using namespace iox::cli::internal; + +template +class OptionTest : public Test +{ + public: + typename T::Type_t createEmpty() + { + return T::createEmpty(); + } +}; + +struct OptionFactory +{ + using Type_t = Option; + static Type_t createEmpty() + { + return Type_t(); + } +}; + +struct OptionWithDetailsFactory +{ + using Type_t = OptionWithDetails; + static Type_t createEmpty() + { + return Type_t{{}, "", iox::cli::OptionType::SWITCH, ""}; + } +}; + + +using Implementations_t = Types; +TYPED_TEST_SUITE(OptionTest, Implementations_t, ); + +TYPED_TEST(OptionTest, emptyOptionIsDetected) +{ + ::testing::Test::RecordProperty("TEST_ID", "7d6d45f3-4fb4-45d2-b968-a5723665d356"); + EXPECT_TRUE(this->createEmpty().isEmpty()); +} + +TYPED_TEST(OptionTest, optionWithLongOptionIsNotEmpty) +{ + ::testing::Test::RecordProperty("TEST_ID", "03557f6a-38b6-42ab-8660-ae21aa218da3"); + auto sut = this->createEmpty(); + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "TheLeafSheepWantsToBeYourFriend"); + EXPECT_FALSE(sut.isEmpty()); +} + +TYPED_TEST(OptionTest, optionWithShortOptionIsNotEmpty) +{ + ::testing::Test::RecordProperty("TEST_ID", "9fe49b82-05e7-47fe-bd5f-286878cd4198"); + auto sut = this->createEmpty(); + sut.shortOption = 'a'; + EXPECT_FALSE(sut.isEmpty()); +} + +TYPED_TEST(OptionTest, optionWithShortAndLongOptionIsNotEmpty) +{ + ::testing::Test::RecordProperty("TEST_ID", "b25ddf2d-e105-44e5-a313-3cc13bc9ad06"); + auto sut = this->createEmpty(); + sut.shortOption = 'b'; + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "PleaseDoNotPetTheHypnotoad"); + EXPECT_FALSE(sut.isEmpty()); +} + +TYPED_TEST(OptionTest, emptyLongOptionDoesNotStartWithDash) +{ + ::testing::Test::RecordProperty("TEST_ID", "533a66ac-0955-4f9b-8243-02e89257a89f"); + auto sut = this->createEmpty(); + EXPECT_FALSE(sut.longOptionNameDoesStartWithDash()); +} + +TYPED_TEST(OptionTest, longOptionWithoutDashDoesNotStartWithDash) +{ + ::testing::Test::RecordProperty("TEST_ID", "88cafc0a-2d20-4d13-9ba0-01549064cad3"); + auto sut = this->createEmpty(); + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "WhyDoesDonaldDuckNeverWearsPants?"); + EXPECT_FALSE(sut.longOptionNameDoesStartWithDash()); +} + +TYPED_TEST(OptionTest, longOptionWithLeadingDashIsDetected) +{ + ::testing::Test::RecordProperty("TEST_ID", "4ca0fd4f-dfc3-45f3-9ff7-0ed83acf410c"); + auto sut = this->createEmpty(); + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "-dashOhNo"); + EXPECT_TRUE(sut.longOptionNameDoesStartWithDash()); +} + +TYPED_TEST(OptionTest, emptyShortOptionDoesNotStartWithDash) +{ + ::testing::Test::RecordProperty("TEST_ID", "0a90616d-8cc6-4513-b67d-23ac0a676ab1"); + auto sut = this->createEmpty(); + EXPECT_FALSE(sut.shortOptionNameIsEqualDash()); +} + +TYPED_TEST(OptionTest, shortOptionWithDashIsDetected) +{ + ::testing::Test::RecordProperty("TEST_ID", "8c666bd8-250b-4ca1-b7a5-f633086088f5"); + auto sut = this->createEmpty(); + sut.shortOption = '-'; + EXPECT_TRUE(sut.shortOptionNameIsEqualDash()); +} + +TYPED_TEST(OptionTest, shortOptionWithNonDashIsHandledCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "51163561-5933-4dae-93f2-d2d394b7e82c"); + auto sut = this->createEmpty(); + sut.shortOption = 'c'; + EXPECT_FALSE(sut.shortOptionNameIsEqualDash()); +} + +TYPED_TEST(OptionTest, hasSameLongOptionNameFailsWithWhenBothAreEmpty) +{ + ::testing::Test::RecordProperty("TEST_ID", "e8c92dbf-fef7-4766-917a-9bd9f5dece7b"); + auto sut = this->createEmpty(); + EXPECT_FALSE(sut.hasLongOptionName("")); +} + +TYPED_TEST(OptionTest, hasSameLongOptionNameFailsWhenBothAreDifferent) +{ + ::testing::Test::RecordProperty("TEST_ID", "fe0d1aca-57fb-4841-b5dd-14d97a15c871"); + auto sut = this->createEmpty(); + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "ChemistryIsTheArt..."); + EXPECT_FALSE(sut.hasLongOptionName("...OfTastingAPlum")); +} + +TYPED_TEST(OptionTest, hasSameLongOptionNameWorksWhenBothAreEqual) +{ + ::testing::Test::RecordProperty("TEST_ID", "65b2adc8-c35b-422f-bf57-7816faf3b18f"); + auto sut = this->createEmpty(); + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "IWouldBeMoreProductiveOnHawaii"); + EXPECT_TRUE(sut.hasLongOptionName("IWouldBeMoreProductiveOnHawaii")); +} + +TYPED_TEST(OptionTest, hasSameShortOptionNameFailsWhenBothAreEmpty) +{ + ::testing::Test::RecordProperty("TEST_ID", "91c0c062-71c5-4e32-9545-ad93e7dd0f66"); + auto sut = this->createEmpty(); + EXPECT_FALSE(sut.hasShortOptionName(iox::cli::NO_SHORT_OPTION)); +} + +TYPED_TEST(OptionTest, hasSameShortOptionNameFailsWhenBothAreDifferent) +{ + ::testing::Test::RecordProperty("TEST_ID", "767ad490-c3b5-4ec3-b7fa-73da203dac96"); + auto sut = this->createEmpty(); + sut.shortOption = 'x'; + EXPECT_FALSE(sut.hasShortOptionName('9')); +} + +TYPED_TEST(OptionTest, hasSameShortOptionNameWorksWhenBothAreEqual) +{ + ::testing::Test::RecordProperty("TEST_ID", "498702db-7603-4051-b150-e4b0155faa79"); + auto sut = this->createEmpty(); + sut.shortOption = '3'; + EXPECT_TRUE(sut.hasShortOptionName('3')); +} + +TYPED_TEST(OptionTest, hasOptionNameFailsWhenBothAreEmpty) +{ + ::testing::Test::RecordProperty("TEST_ID", "37396c79-683b-4923-a133-a678f03c484c"); + auto sut = this->createEmpty(); + EXPECT_FALSE(sut.hasOptionName("")); +} + +TYPED_TEST(OptionTest, hasOptionNameWorksWhenEqualToLongOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "b55c180c-bb2d-470c-91bf-a1c0c06c3aab"); + auto sut = this->createEmpty(); + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "AskYourselfWhatWouldHypnotoadDo"); + EXPECT_TRUE(sut.hasOptionName("AskYourselfWhatWouldHypnotoadDo")); +} + +TYPED_TEST(OptionTest, hasOptionNameWorksWhenEqualToShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "fc6e5c05-87d3-406a-b27b-16e9fe9ea73e"); + auto sut = this->createEmpty(); + sut.shortOption = 'j'; + EXPECT_TRUE(sut.hasOptionName("j")); +} + +TYPED_TEST(OptionTest, sameShortAndLongOptionsWithDifferentValueAreTheSameOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "b5a14fc2-d1af-49ee-8fbc-a5850361cf4e"); + auto sut = this->createEmpty(); + sut.shortOption = 'k'; + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "IHateMeetings"); + sut.value = "bla"; + + auto sut2 = this->createEmpty(); + sut2.shortOption = sut.shortOption; + sut2.longOption = sut.longOption; + sut2.value = "WhoCaresAboutLifetime"; + + EXPECT_TRUE(sut.isSameOption(sut2)); + EXPECT_TRUE(sut.isSameOption(sut)); +} + +TYPED_TEST(OptionTest, sameShortOptionDifferentLongOptionAreNotTheSameOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "4b30a170-65cd-4c83-acca-28531acc7167"); + auto sut = this->createEmpty(); + sut.shortOption = 'k'; + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "BlueberrysAreNice"); + sut.value = "bla"; + + auto sut2 = this->createEmpty(); + sut2.shortOption = sut.shortOption; + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "ButWhatAboutTheSwedishWhitebeam"); + sut2.value = "WhoCaresAboutOwnership"; + + EXPECT_FALSE(sut.isSameOption(sut2)); +} + +TYPED_TEST(OptionTest, sameLongOptionDifferentShortOptionAreNotTheSameOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "e158eb85-ed58-45e3-bf35-e64c4a4e83ea"); + auto sut = this->createEmpty(); + sut.shortOption = 'k'; + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "ArnoldSchwarzeneggerIsMozart"); + sut.value = "bla"; + + auto sut2 = this->createEmpty(); + sut2.shortOption = 'c'; + sut.longOption = sut2.longOption; + sut2.value = "LookOverThereAThreeHeadedMonkey"; + + EXPECT_FALSE(sut.isSameOption(sut2)); +} + +TYPED_TEST(OptionTest, emptyOptionHasNoShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "1ea16b12-8d8c-43a9-b09b-fe9cc3094cc8"); + auto sut = this->createEmpty(); + EXPECT_FALSE(sut.hasShortOption()); +} + +TYPED_TEST(OptionTest, setupShortOptionHasShortOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "b5e4dd07-a63e-4aae-90dd-a1c9ee1145db"); + auto sut = this->createEmpty(); + sut.shortOption = 'p'; + EXPECT_TRUE(sut.hasShortOption()); +} + +TYPED_TEST(OptionTest, emptyOptionHasNoLongOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "55b459dd-bdc9-41db-8b1f-55ffb0b94bee"); + auto sut = this->createEmpty(); + EXPECT_FALSE(sut.hasLongOption()); +} + +TYPED_TEST(OptionTest, setupLongOptionHasLongOption) +{ + ::testing::Test::RecordProperty("TEST_ID", "02b4a288-9e52-4668-bb16-0a87706c5095"); + auto sut = this->createEmpty(); + sut.longOption = iox::cli::OptionName_t(iox::cxx::TruncateToCapacity, "MozartHadASon"); + EXPECT_TRUE(sut.hasLongOption()); +} + +TYPED_TEST(OptionTest, lessOperatorWorksWithTwoShortOptions) +{ + ::testing::Test::RecordProperty("TEST_ID", "f189b3b3-818d-4044-a7ba-1a23395d52fd"); + + auto sut1 = this->createEmpty(); + sut1.shortOption = '1'; + + auto sut2 = this->createEmpty(); + sut2.shortOption = '2'; + + EXPECT_TRUE(sut1 < sut2); + EXPECT_FALSE(sut2 < sut1); +} + +TYPED_TEST(OptionTest, lessOperatorWorksWithMixedOptionTypes) +{ + ::testing::Test::RecordProperty("TEST_ID", "dee26761-b9b4-4d82-ba26-fb36605d4d0d"); + + auto sut1 = this->createEmpty(); + sut1.shortOption = '3'; + + auto sut2 = this->createEmpty(); + sut2.longOption = "444"; + + EXPECT_TRUE(sut1 < sut2); + EXPECT_FALSE(sut2 < sut1); +} + +TYPED_TEST(OptionTest, lessOperatorWorksWithTwoLongOptions) +{ + ::testing::Test::RecordProperty("TEST_ID", "db398659-37d4-4df9-98ec-b06fddb533cb"); + + auto sut1 = this->createEmpty(); + sut1.longOption = "555"; + + auto sut2 = this->createEmpty(); + sut2.longOption = "666"; + + EXPECT_TRUE(sut1 < sut2); + EXPECT_FALSE(sut2 < sut1); +} + +} // namespace diff --git a/iceoryx_dust/test/moduletests/test_cli_option_definition.cpp b/iceoryx_dust/test/moduletests/test_cli_option_definition.cpp new file mode 100644 index 0000000000..4612972eb5 --- /dev/null +++ b/iceoryx_dust/test/moduletests/test_cli_option_definition.cpp @@ -0,0 +1,189 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_dust/internal/cli/option_definition.hpp" +#include "iceoryx_hoofs/error_handling/error_handling.hpp" +#include "iceoryx_hoofs/testing/mocks/error_handler_mock.hpp" +#include "test.hpp" +#include "test_cli_command_line_common.hpp" + +#include +#include +#include +#include + +namespace +{ +using namespace ::testing; +using namespace iox::cli; +using namespace iox::cli::internal; +using namespace iox::cxx; + +/// All the success tests are handled indirectly in the CommandLineArgumentParser_test +/// where every combination of short and long option is parsed and verified +class OptionDefinition_test : public Test +{ + public: + void SetUp() override + { + // if we do not capture stdout then the console is filled with garbage + // since the command line parser prints the help on failure + ::testing::internal::CaptureStdout(); + } + void TearDown() override + { + std::string output = ::testing::internal::GetCapturedStdout(); + if (Test::HasFailure()) + { + std::cout << output << std::endl; + } + } + + uint64_t numberOfErrorCallbackCalls = 0U; + iox::cxx::function errorCallback = [this] { ++numberOfErrorCallbackCalls; }; + static Argument_t defaultValue; +}; +Argument_t OptionDefinition_test::defaultValue = "DEFAULT VALUE"; + +TEST_F(OptionDefinition_test, AddingTheSameShortOptionLeadsToExit) +{ + ::testing::Test::RecordProperty("TEST_ID", "f1340876-e3f6-4f62-b0f3-4e9551a5f67a"); + OptionDefinition optionSet("", errorCallback); + optionSet.addOptional('c', "firstEntry", "", "", ""); + + optionSet.addSwitch('c', "duplicateShortOption", ""); + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); + + optionSet.addOptional('c', "duplicateShortOption", "", "", ""); + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(2)); + + optionSet.addRequired('c', "duplicateShortOption", "", ""); + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(3)); +} + +TEST_F(OptionDefinition_test, AddingTheSameLongOptionLeadsToExit) +{ + ::testing::Test::RecordProperty("TEST_ID", "076b8877-e3fc-46f7-851b-d3e7953f67d6"); + OptionDefinition optionSet("", errorCallback); + optionSet.addSwitch('c', "duplicate", ""); + + optionSet.addSwitch('x', "duplicate", ""); + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); + + optionSet.addOptional('x', "duplicate", "", "", ""); + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(2)); + + optionSet.addRequired('x', "duplicate", "", ""); + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(3)); +} + +TEST_F(OptionDefinition_test, AddingOptionWithSameShortAndLongNameLeadsToExit) +{ + ::testing::Test::RecordProperty("TEST_ID", "4e01ed47-473d-4915-aed2-60aacce37de8"); + OptionDefinition optionSet("", errorCallback); + optionSet.addRequired('d', "duplicate", "", ""); + + optionSet.addSwitch('d', "duplicate", ""); + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); + + optionSet.addOptional('d', "duplicate", "", "", ""); + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(2)); + + optionSet.addRequired('d', "duplicate", "", ""); + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(3)); +} + +TEST_F(OptionDefinition_test, AddingSwitchWithDashAsShortOptionLeadsToFailure) +{ + ::testing::Test::RecordProperty("TEST_ID", "5c6558ec-ecd9-47e9-b396-593445cef68f"); + OptionDefinition optionSet("", errorCallback); + optionSet.addSwitch('-', "", ""); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} + +TEST_F(OptionDefinition_test, AddingOptionalValueWithDashAsShortOptionLeadsToFailure) +{ + ::testing::Test::RecordProperty("TEST_ID", "8afd403b-9a77-4bde-92df-0200d4fb661b"); + OptionDefinition optionSet("", errorCallback); + optionSet.addOptional('-', "", "", "", ""); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} + +TEST_F(OptionDefinition_test, AddingRequiredValueWithDashAsShortOptionLeadsToFailure) +{ + ::testing::Test::RecordProperty("TEST_ID", "04e358dd-6ef4-48e4-988e-ee1d0514632b"); + OptionDefinition optionSet("", errorCallback); + optionSet.addRequired('-', "", "", ""); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} + +TEST_F(OptionDefinition_test, AddingSwitchWithDashStartingLongOptionLeadsToFailure) +{ + ::testing::Test::RecordProperty("TEST_ID", "62c0882a-7055-4a74-9dd9-8505d72da1e0"); + OptionDefinition optionSet("", errorCallback); + optionSet.addSwitch('a', "-oh-no-i-start-with-dash", ""); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} + +TEST_F(OptionDefinition_test, AddingOptionalValueWithDashStartingLongOptionLeadsToFailure) +{ + ::testing::Test::RecordProperty("TEST_ID", "69c975d1-57d3-429a-b894-7ff1efa9f473"); + OptionDefinition optionSet("", errorCallback); + optionSet.addOptional('c', "-whoopsie-there-is-a-dash", "", "", ""); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} + +TEST_F(OptionDefinition_test, AddingRequiredValueWithDashStartingLongOptionLeadsToFailure) +{ + ::testing::Test::RecordProperty("TEST_ID", "43929047-1051-45cd-8a13-ebf8ea8c4e26"); + OptionDefinition optionSet("", errorCallback); + optionSet.addRequired('b', "-dash-is-all-i-need", "", ""); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} + +TEST_F(OptionDefinition_test, AddingSwitchWithEmptyShortAndLongOptionLeadsToFailure) +{ + ::testing::Test::RecordProperty("TEST_ID", "f1aa3314-0355-43d8-85b3-b2e7d604440e"); + OptionDefinition optionSet("", errorCallback); + optionSet.addSwitch(NO_SHORT_OPTION, "", ""); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} + +TEST_F(OptionDefinition_test, AddingOptionalWithEmptyShortAndLongOptionLeadsToFailure) +{ + ::testing::Test::RecordProperty("TEST_ID", "ee6866b7-a4f8-4406-ab50-ba0d1b798696"); + OptionDefinition optionSet("", errorCallback); + optionSet.addOptional(NO_SHORT_OPTION, "", "", "", ""); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} + +TEST_F(OptionDefinition_test, AddingRequiredValueWithEmptyShortAndLongOptionLeadsToFailure) +{ + ::testing::Test::RecordProperty("TEST_ID", "bf33281b-de11-4482-8bc4-1e443c5b3bc1"); + OptionDefinition optionSet("", errorCallback); + optionSet.addRequired(NO_SHORT_OPTION, "", "", ""); + + EXPECT_THAT(numberOfErrorCallbackCalls, Eq(1)); +} +} // namespace diff --git a/iceoryx_hoofs/CMakeLists.txt b/iceoryx_hoofs/CMakeLists.txt index 7ebed43881..23a0f5d7d9 100644 --- a/iceoryx_hoofs/CMakeLists.txt +++ b/iceoryx_hoofs/CMakeLists.txt @@ -39,7 +39,6 @@ set(PREFIX iceoryx/v${CMAKE_PROJECT_VERSION}) # ########## build iceoryx hoofs lib ########## # - iox_add_library( TARGET iceoryx_hoofs NAMESPACE iceoryx_hoofs @@ -65,6 +64,7 @@ iox_add_library( source/cxx/functional_interface.cpp source/cxx/helplets.cpp source/cxx/requires.cpp + source/cxx/type_traits.cpp source/cxx/unique_id.cpp source/error_handling/error_handler.cpp source/error_handling/error_handling.cpp diff --git a/iceoryx_hoofs/include/iceoryx_hoofs/cxx/type_traits.hpp b/iceoryx_hoofs/include/iceoryx_hoofs/cxx/type_traits.hpp index 93d2d4ed70..ea070958bb 100644 --- a/iceoryx_hoofs/include/iceoryx_hoofs/cxx/type_traits.hpp +++ b/iceoryx_hoofs/include/iceoryx_hoofs/cxx/type_traits.hpp @@ -159,6 +159,119 @@ struct is_cxx_string<::iox::cxx::string> : std::true_type template using void_t = void; + +////////////////// +/// BEGIN TypeInfo +////////////////// + +/// @brief Provides a translation from a type into its human readable name +/// NOLINTJUSTIFICATION The name should be stored in a compile time variable. Access is always +/// safe since it is null terminated and always constant. Other alternatives are not available +/// at compile time. +/// NOLINTBEGIN(hicpp-avoid-c-arrays, cppcoreguidelines-avoid-c-arrays) + +template +struct TypeInfo +{ + static_assert(always_false_v, "unknown type"); + static constexpr const char NAME[] = "unknown type"; +}; +template +constexpr const char TypeInfo::NAME[]; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "int8_t"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "int16_t"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "int32_t"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "int64_t"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "uint8_t"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "uint16_t"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "uint32_t"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "uint64_t"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "bool"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "char"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "float"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "double"; +}; + +template <> +struct TypeInfo +{ + static constexpr const char NAME[] = "long double"; +}; + +template +class string; + +template +struct TypeInfo> +{ + static constexpr const char NAME[] = "string"; +}; +template +constexpr const char TypeInfo>::NAME[]; +/// NOLINTEND(hicpp-avoid-c-arrays, cppcoreguidelines-avoid-c-arrays) +////////////////// +/// END TypeInfo +////////////////// + } // namespace cxx } // namespace iox diff --git a/iceoryx_hoofs/source/cxx/type_traits.cpp b/iceoryx_hoofs/source/cxx/type_traits.cpp new file mode 100644 index 0000000000..3a797b670f --- /dev/null +++ b/iceoryx_hoofs/source/cxx/type_traits.cpp @@ -0,0 +1,40 @@ +// Copyright (c) 2022 by Apex.AI Inc. 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "iceoryx_hoofs/cxx/type_traits.hpp" + +namespace iox +{ +namespace cxx +{ +/// NOLINTJUSTIFICATION See definitions in header file. +/// NOLINTBEGIN(hicpp-avoid-c-arrays, cppcoreguidelines-avoid-c-arrays) +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +constexpr const char TypeInfo::NAME[]; +/// NOLINTEND(hicpp-avoid-c-arrays, cppcoreguidelines-avoid-c-arrays) +} // namespace cxx +} // namespace iox diff --git a/iceoryx_hoofs/test/moduletests/test_cxx_type_traits.cpp b/iceoryx_hoofs/test/moduletests/test_cxx_type_traits.cpp index 60930864d7..2a6c629273 100644 --- a/iceoryx_hoofs/test/moduletests/test_cxx_type_traits.cpp +++ b/iceoryx_hoofs/test/moduletests/test_cxx_type_traits.cpp @@ -171,6 +171,91 @@ TEST(TypeTraitsTest, IsFunctionPointerResolvesToFalse) EXPECT_FALSE(result); } +TEST(TypeTraitsTest, TypeInfo_StringTypeTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "e20e6698-3c0c-4b28-a8bb-f5c5dd05a107"); + EXPECT_THAT(TypeInfo>::NAME, StrEq("string")); + EXPECT_THAT(TypeInfo>::NAME, StrEq("string")); +} + +TEST(TypeTraitsTest, TypeInfo_int8_tTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "50fca418-002f-43c6-8899-61377b43b96a"); + EXPECT_THAT(TypeInfo::NAME, StrEq("int8_t")); +} + +TEST(TypeTraitsTest, TypeInfo_int16_tTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "bd0e39dc-7950-4f55-a284-1265570e3e46"); + EXPECT_THAT(TypeInfo::NAME, StrEq("int16_t")); +} + +TEST(TypeTraitsTest, TypeInfo_int32_tTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "0c988179-dd46-4d42-98cc-b12ca3702518"); + EXPECT_THAT(TypeInfo::NAME, StrEq("int32_t")); +} + +TEST(TypeTraitsTest, TypeInfo_int64_tTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "2c5fceb1-2dd3-4133-b968-d0509d04e3d7"); + EXPECT_THAT(TypeInfo::NAME, StrEq("int64_t")); +} + +TEST(TypeTraitsTest, TypeInfo_uint8_tTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "a14d787b-ca2c-4e14-b61d-4c7dea7e7c7a"); + EXPECT_THAT(TypeInfo::NAME, StrEq("uint8_t")); +} + +TEST(TypeTraitsTest, TypeInfo_uint16_tTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "581a078a-2541-47b7-a929-29a46e38cee9"); + EXPECT_THAT(TypeInfo::NAME, StrEq("uint16_t")); +} + +TEST(TypeTraitsTest, TypeInfo_uint32_tTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "a0012e71-8f5b-4979-a56b-400533236c8a"); + EXPECT_THAT(TypeInfo::NAME, StrEq("uint32_t")); +} + +TEST(TypeTraitsTest, TypeInfo_uint64_tTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "6c9d41b0-9e5a-45a1-830d-bf46507a0000"); + EXPECT_THAT(TypeInfo::NAME, StrEq("uint64_t")); +} + +TEST(TypeTraitsTest, TypeInfo_boolTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "7506bc90-1447-48d9-ae91-621d0f4c1db2"); + EXPECT_THAT(TypeInfo::NAME, StrEq("bool")); +} + +TEST(TypeTraitsTest, TypeInfo_charTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "2aa53aa6-b2b0-4a78-bb58-1cabfb695e8b"); + EXPECT_THAT(TypeInfo::NAME, StrEq("char")); +} + +TEST(TypeTraitsTest, TypeInfo_floatTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "b90f78d7-7b1f-49c1-ad90-4d5e706c63ae"); + EXPECT_THAT(TypeInfo::NAME, StrEq("float")); +} + +TEST(TypeTraitsTest, TypeInfo_doubleTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "222baf0b-ae93-4c25-9244-18d4451a7e4f"); + EXPECT_THAT(TypeInfo::NAME, StrEq("double")); +} + +TEST(TypeTraitsTest, TypeInfo_long_doubleTranslatesCorrectly) +{ + ::testing::Test::RecordProperty("TEST_ID", "49fd3664-9d03-48b8-9c61-8f700c51194d"); + EXPECT_THAT(TypeInfo::NAME, StrEq("long double")); +} + TEST(TypeTraitsTest, NonCharArraysAreIdentifiedCorrectly) { ::testing::Test::RecordProperty("TEST_ID", "40359de0-2ccd-422a-b1d4-da4b4f12a172"); @@ -214,6 +299,4 @@ TEST(TypeTraitsTest, CxxStringsAreIdentifiedCorrectly) EXPECT_TRUE(is_cxx_string>::value); EXPECT_TRUE(is_cxx_string>::value); } - - } // namespace diff --git a/iceoryx_platform/win/include/iceoryx_platform/platform_correction.hpp b/iceoryx_platform/win/include/iceoryx_platform/platform_correction.hpp index d9c79f8575..07b1121f9a 100644 --- a/iceoryx_platform/win/include/iceoryx_platform/platform_correction.hpp +++ b/iceoryx_platform/win/include/iceoryx_platform/platform_correction.hpp @@ -40,6 +40,8 @@ #undef CreateSemaphore #undef NO_ERROR #undef OPEN_EXISTING +#undef IGNORE +#undef OPTIONAL /// @todo iox-#1616 required by posix but defined in libc header string.h /// move it into the upcoming libc layer in platform diff --git a/tools/scripts/check_test_ids.sh b/tools/scripts/check_test_ids.sh index 45b8becb77..991c6c5cc9 100755 --- a/tools/scripts/check_test_ids.sh +++ b/tools/scripts/check_test_ids.sh @@ -21,18 +21,24 @@ set -e +ICEORYX_PATH=$(git rev-parse --show-toplevel) +cd $ICEORYX_PATH + ## sanity check for number of tests and test IDs -numberOfTestCases=$(grep -rn --include="*.cpp" -e "^\(TEST\|TYPED_TEST\|TIMING_TEST\)" iceoryx_* | wc -l) -numberOfFalsePositives=$(grep -rn --include="*.cpp" TYPED_TEST_SUITE iceoryx_* | wc -l) -numberOfTestCasesWithoutFalsePositives=$numberOfTestCases-$numberOfFalsePositives -numberOfTestIDs=$(grep -rn --include="*.cpp" "::testing::Test::RecordProperty" iceoryx_* | wc -l) -if [[ "$numberOfTestCasesWithoutFalsePositives" -gt "$numberOfTestIDs" ]]; then - echo -e "\e[1;31mThe number of test IDs do not match the number of test cases!\e[m" - echo "number of test cases: $numberOfTestCases" - echo "number of false positives: $numberOfFalsePositives" - echo "number of test IDs: $numberOfTestIDs" - exit 1 -fi +for file in $(find . -iname "*.cpp") +do + echo "Check file: $file" + numberOfTestCases=$(grep -rn --include="*.cpp" -e "^\(TEST\|TYPED_TEST\|TIMING_TEST\)" $file | wc -l) + numberOfFalsePositives=$(grep -rn --include="*.cpp" TYPED_TEST_SUITE $file | wc -l) + set +e + numberOfTestCasesWithoutFalsePositives=$(expr $numberOfTestCases - $numberOfFalsePositives) + set -e + numberOfTestIDs=$(grep -rn --include="*.cpp" "::testing::Test::RecordProperty" $file | wc -l) + if [[ "$numberOfTestCasesWithoutFalsePositives" -gt "$numberOfTestIDs" ]]; then + echo -e "\e[1;31mThe file \"$file\" is missing test IDs for some test cases!\e[m" + exit 1 + fi +done ## unique test IDs notUniqueIds=$(grep -hr "::testing::Test::RecordProperty" | sort | uniq -d)