diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25a06b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +*.swp +compile_commands.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5a4e85a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/googletest"] + path = third_party/googletest + url = https://github.com/google/googletest.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..51e095c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,55 @@ +# Copyright 2019 IBM Corporation +# +# 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. + +cmake_minimum_required (VERSION 2.8) +project (gWhisper) + +if(DEFINED CMAKE_BUILD_TYPE) +else() + # By default we build in Debug mode. For releases, + # please run CMAKE with + # -DCMAKE_BUILD_TYPE=RelWithDebInfo + + # non optimized with debug symbols: + set(CMAKE_BUILD_TYPE Debug) +endif() + +if (CMAKE_VERSION VERSION_LESS "3.1") + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11") + else() + set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + endif() +else() + set(CMAKE_CXX_STANDARD 11) +endif() + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +enable_testing() + +if(BUILD_CONFIG_USE_BOOST_REGEX) + add_definitions(-DBUILD_CONFIG_USE_BOOST_REGEX) +endif() + +# this causes all built executables to be on build directory toplevel. +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} ) + +include_directories("${PROJECT_BINARY_DIR}") +include_directories("${PROJECT_BINARY_DIR}/src") +include_directories("${PROJECT_SOURCE_DIR}") +include_directories("${PROJECT_SOURCE_DIR}/src") + +add_subdirectory(src) +add_subdirectory(tests) +add_subdirectory(third_party) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4a19045 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,151 @@ +# Contributing +## Contributing In General +Our project welcomes external contributions. If you have an itch, please feel +free to scratch it. + +To contribute code or documentation, please submit a [pull request](https://github.com/ibm/gWhisper/pulls). + +A good way to familiarize yourself with the codebase and contribution process is +to look for and tackle low-hanging fruit in the [issue tracker](https://github.com/ibm/gWhisper/issues). +Before embarking on a more ambitious contribution, please quickly [get in touch](#communication) with us and have a look at the [PROJECT_SCOPE.md](PROJECT_SCOPE.md). + +**Note: We appreciate your effort, and want to avoid a situation where a contribution +requires extensive rework (by you or by us), sits in backlog for a long time, or +cannot be accepted at all!** + +### Proposing new features + +If you would like to implement a new feature, please [raise an issue](https://github.com/ibm/gWhisper/issues) +before sending a pull request so the feature can be discussed. This is to avoid +you wasting your valuable time working on a feature that the project developers +are not interested in accepting into the code base. + +### Fixing bugs + +If you would like to fix a bug, please [raise an issue](https://github.com/ibm/gWhisper/issues) before sending a +pull request so it can be tracked. + +### Merge approval + +The project maintainers use LGTM (Looks Good To Me) in comments on the code +review to indicate acceptance. A change requires LGTMs from one of the +maintainers of each component affected. + +For a list of the maintainers, see the [MAINTAINERS.md](MAINTAINERS.md) page. + +## Communication +For feature requests, bug reports or technical discussions, please use the [issue tracker](https://github.com/ibm/gWhisper/issues). + +Depending on the need, we might create a channel on matrix.org (see [riot.im](https://about.riot.im/)) or on slack for general questions. In the meantime please contact one of the [maintainers](MAINTAINERS.md) directly for general questions or feedback. + +## Testing +As we currently do not have a CI and test framework set up, but plan to do so in the future, +we encourage contributors to ensure that existing unit-tests pass by running `make test` before +submitting a pull request. Also it is highly appreciated if you implement unit-tests +for new code. + +## Coding style guidelines +Existing code might not always follow the guidelines listed below. However please +write new code to correspond to these guidelines and feel free to re-factor old +code to be compliant. + +### Directory structure +We are trying to stay close to _Canonical Project Structure_ as described [in this proposal](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1204r0.html). +All new code should follow the directory and naming conventions described here. + +### Indentation +This project is indented using spaces. Tabs are not allowed. One level of indentation corresponds to 4 spaces. +``` +void myMethod(bool f_condition) +{ + ... + if(f_condition) + { + if(otherCondition) + { + doSomething(); + } + } +} +``` + +### Naming conventions +- Classes: + UpperCamelCase +- Methods: + lowerCamelCase +- Variables: + lowerCamelCase + + Scope for variables is indicated by prefixes: +``` + g_ -> global scope + m_ -> object member variable + f_ -> variable given as method/function argument + f_out -> variable given as method/function argument, out parameter. + I.e. caller expects function to write/modify data referenced + to by this variable. +``` + +### Scoping brackets +The curly bracket at scope start and scope end should have the same indentation level and immediately be followed by a new-line: +``` +if(condition) +{ + something(); +} +``` + +### Source code documentation +For documenting code, please use the [doxygen style](http://www.doxygen.nl/manual/docblocks.html) method 3. +For example to document a method: +``` +/// Method for doing my stuff. +/// This method is doing my stuff by doing .... +myMethod() +... +``` + +## Legal + +Each source file must include a license header for the Apache +Software License 2.0: + +``` +Copyright [yyyy] [name of copyright owner] + +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. +``` + +We have tried to make it as easy as possible to make contributions. This +applies to how we handle the legal aspects of contribution. We use the +same approach - the [Developer's Certificate of Origin 1.1 (DCO)](DCO1.1.txt) - that the Linux® Kernel [community](https://elinux.org/Developer_Certificate_Of_Origin) +uses to manage code contributions. + +We simply ask that when submitting a patch for review, the developer +must include a sign-off statement in the commit message. + +Here is an example Signed-off-by line, which indicates that the +submitter accepts the DCO: + +``` +Signed-off-by: John Doe +``` + +You can include this automatically when you commit a change to your +local git repository using the following command: + +``` +git commit -s +``` + diff --git a/DCO1.1.txt b/DCO1.1.txt new file mode 100644 index 0000000..f440e6f --- /dev/null +++ b/DCO1.1.txt @@ -0,0 +1,25 @@ +Developer's Certificate of Origin 1.1 + + By making a contribution to this project, I certify that: + + (a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + + (b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + + (c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + + (d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..5b288cd --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,6 @@ +# MAINTAINERS + +Rainer Schoenberger - rschoe@de.ibm.com + +Eric Boehmler - erboh@de.ibm.com + diff --git a/PROJECT_SCOPE.md b/PROJECT_SCOPE.md new file mode 100644 index 0000000..97db1fa --- /dev/null +++ b/PROJECT_SCOPE.md @@ -0,0 +1,59 @@ +# Scope and design goals of the gWhisper CLI project +This document defines the scope of gWhisper. It should be helpful to +decide if a feature request or pull request will be accepted or not. + +However if you have ideas which do not quite fit the scope and you are willing to implement +them, feel free to start a discussion. + +## User personas +The scope of the project is probably defined best by looking at the intended users: + +### A gRPC service developer +- Maybe uses gRPC for the first time +- Wants to get to know gRPC by exploring a test server (hello world) +- Wants to test and debug a gRPC service implementation + +### A gRPC API user +- Maybe uses gRPC for the first time +- Has to interface a badly documented service implementation +- Wants to explore a gRPC service, as well as its exported RPCs and message types +- Wants to execute RPCs to learn how the server reacts + +### A person giving technical support +- Has deep knowledge of the service implementations +- Wants to execute RPCs to recover a corrupted server state +- Wants to execute RPCs to trigger actions which are not possible in static client implementations + +## What is important: +- Tab completion + - Big plus in usability + - No need to look at documentation +- Reflection support + - Discoverability: all provided services of the server are offered to the CLI user + - User does not have to care about proto files at all. In fact he or she does + not even need to know what a proto file is :-) +- Colors + - Modern terminals support color + - Complex data structures are much better to read with color highlighting +- Documentation + - Users should not need to use internet search engines or write mails/issues to learn how to use the tool +- The tool should be usable in a terminal + - All intended users are developers and assumed to be familiar with CLI tools + - A CLI tool tends to be more versatile and time-less than graphical interfaces + - Allows flexible use of the tool (e.g. via SSH, in scripts, etc.) + +## Nice to have, but not necessary: +- Custom output formatting + - This tends to let gRPC be used as a production tool, which is not the primary goal of gWhisper +- Syntax compatibility between releases + - This would allow use of the tool in long-living scripts or for _Machine to Machine_ communication production environments, which is not a goal of gWhisper. + +## What is not important: +- Performance for executing gRPC calls + The tool is not intended to be used for executing large numbers of RPCs or + for transfering big chunks of data. +- Output formatting in machine readable form (think JSON, XML, etc) + The tool is intended as a User Interface not for use in production environments. + If it is desired to process replies of a server with an algorithm, users + should directly implement their own gRPC client. (gRPC provides excellent + scripting interfaces) diff --git a/README.md b/README.md index 68d47bd..6be8a8e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,108 @@ -# gWhisper -gWhisper is a gRPC command-line client. -It allows to invoke gRPC calls from the command-line. - +# gWhisper - A gRPC command line tool +A gRPC command line client. +It allows to invoke gRPC Calls from the commandline and formats the replies +in a human readable format. + +![example invocation](example.gif) + The main design goals are: + - Reflection support (no proto files required) -- Tab-Completion for services, methods __and their arguments__ - (currently only supported in Bash) -- Human readable output in color +- Tab completion for + - services + - methods + - method arguments (currently only supported in Bash) + including nested types - Usable directly in the shell - -The code for this tool will be released to the public soon under the Apache v2.0 license. +- Designed with usability in mind + +Have a look at the [project scope](PROJECT_SCOPE.md) for details. + +Synopsis: + + gwhisper [OPTIONS] [:port] [=FIELD_VALUE ]... + +Execute `gwhisper --help` or click [here](doc/Usage.txt) to get detailed information and examples on how to use the tool. + +## Download gWhisper + +Clone the repository + + git clone https://github.com/IBM/gWhisper.git + +Initialize third-party submodules (currently only "google test") + + cd gWhisper + git submodule update --init + +NOTE: Please do not download gWhisper as ZIP from GitHub. GitHub currently does not support submodules, which is why you will end up with an incomplete codebase. + +## Prerequisites + +To be able to build and/or run gWhisper, you need to at least have the following dependencies installed on your system: + +- cmake +- A C++ compiler +- gRPC [link](https://github.com/grpc/grpc) + including the protoc plugin, which is packaged separately in some linux distributions +- protocolBuffers [link](https://github.com/protocolbuffers/protobuf) + +On Fedora you can install the prerequisites with: + + yum install cmake gcc-c++ protoc grpc grpc-devel grpc-plugins + +On other distributions we tried, gRPC and/or protobuf packages seem to be not available, outdated or incomplete (missing gRPC protoc plugin). +In this case, please build and install gRPC and protocolBuffers from the official sources. + +## Build and run + +Build the code + + ./build.sh + +Source the bash completion file (for tab completion) + + . ./complete.bash + +Run the executable (use TabCompletion): + + ./build/gwhisper [] + +NOTE: +If you are not building in a checked out git repository you should set the environment variable `GWHISPER_BUILD_VERSION` to the appropriate version of the source code. +This will end up as part of the version string, returned when calling `gWhisper --version`. +In case you are building within the git repository, the version is automatically determined during the build. You may however add additional information to the version string by using this environment variable. + +## Current development status + +This is in a pre-release state. Basic functionality is implemented, but you may experience bugs. +Feel free to try it out and provide feedback/contributions. + +What is working: + +- Tab Completion (bash only) +- Calling RPCs (unary + server-streaming) +- Output of all types supported by protocol buffers + +Some notable things which are not yet working: + +- Input: OneOf fields +- Input: Client streaming RPCs +- Input: Escaping of control characters (":@.(,") +- Completion: Support for shells other than BASH (e.g. zsh, fish) +- Security: Authentication / Encryption of channels +- Performance: Caching of reflection queries + +## Supported platforms + +All development and testing is done on ubuntu linux (mostly 16.04). We expect no bigger problems +with building and running this software on different linux distributions. + +## Reporting issues + +Please use the GitGub [issues tab](https://github.com/ibm/gWhisper/issues). +Be sure to search issues first to avoid duplicate entries. + +## Contribute + +Please have a look at [CONTRIBUTE.md](CONTRIBUTING.md). diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..eb8b2be --- /dev/null +++ b/build.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Copyright 2019 IBM Corporation +# +# 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. + +SOURCE_DIR="$PWD" +BUILD_DIR="$SOURCE_DIR/build" +if [ ! -f "$BUILD_DIR/Makefile" ]; then + mkdir $BUILD_DIR + cd $BUILD_DIR + cmake $SOURCE_DIR "$@" + cd $SOURCE_DIR +fi +cd $BUILD_DIR +make -j8 + diff --git a/complete.bash b/complete.bash new file mode 100755 index 0000000..09d144e --- /dev/null +++ b/complete.bash @@ -0,0 +1,70 @@ +# Copyright 2019 IBM Corporation +# +# 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. + +# This is a very simple completion function. It essentially offloads all +# completion work to the gWhisper tool itself, by issuing a completion request. +# The output of gWhisper is then used to fill the COMPREPLY variable. +_gWhisperCompletion() +{ + # we interpret the first word as the gWhisper command itself: + COMMANDNAME=${COMP_WORDS[0]} + if [ $GWHISPER_DEBUG_COMPLETION ] + then + echo "## completion for command: ######################################" + echo "$COMMANDNAME" + fi + + # index of the first character of command arguments + ARG_START=$(( ${#COMMANDNAME} + 1 )) + # length of characters in arguments until (excluding) cursor position + ARG_LEN=$(( $COMP_POINT - $ARG_START )) + + ARGS=${COMP_LINE:$ARG_START:$ARG_LEN} + if [ $GWHISPER_DEBUG_COMPLETION ] + then + echo "## arguments to complete: #######################################" + echo "\"$ARGS\"" + fi + + # we retrieve completion choices by just executing gWhisper with the + # --complete argument in the beginning: + SUGGESTIONS=$($COMMANDNAME "--complete $ARGS") + + if [ $GWHISPER_DEBUG_COMPLETION ] + then + echo "## gWhisper returned the following completion suggestion: ##########" + echo "\"$SUGGESTIONS\"" + fi + + # We parse the suggestions returned by gWhisper into an array. We only split + # on newline, wo we have to change $IFS and restore after the conversion: + OIFS=$IFS; + IFS=$'\n'; # we only split on newlines + COMPREPLY=($SUGGESTIONS) + IFS=$OIFS; + + + if [ $GWHISPER_DEBUG_COMPLETION ] + then + echo "## parsed suggestion string into array: #########################" + for i in "${COMPREPLY[@]}" + do + echo "entry: '$i'" + done + fi + + return 0 +} + +complete -o nospace -F _gWhisperCompletion gwhisper diff --git a/doc/Usage.txt b/doc/Usage.txt new file mode 100644 index 0000000..209c68b --- /dev/null +++ b/doc/Usage.txt @@ -0,0 +1,88 @@ +gWhisper - A gRPC command-line client +This command allows to call gRPC RPCs on any gRPC server with reflection enabled. +You may use TAB completion for all arguments (currently only supported in BASH). + +SYNOPSIS: +gwhisper [OPTIONS] [:port] [=FIELD_VALUE ]... + +The default TCP port used to connect to a gRPC server is 50051. + +OPTIONS: + -h + --help + Shows this help. + --noColor + Disables colors in the output of gRPC replies. + --customOutput OUTPUT_FORMAT + Instead of printing the reply message using the default human readable + format, a custom format as specified in OUTPUT_FORMAT is used. + See OUTPUT_FORMAT section for a description of the OUTPUT_FORMAT language. + Note that this is an experimental feature and will be documented in detail, + once finished. + --complete + Shows possible next arguments. + The output is rendered to be usable as input for bash-completion. + --dot + Prints a graphviz digraph, representing the current grammar of the parser. + +FIELD_VALUE: + Field values in the request message may be specified as follows + - integers: + decimal (e.g. 46, -46, +46) + hexadecimal (e.g. 0x45ab, -0x45ab, +0x45ab) + octal (e.g. 078, -078, +078) + - floats: + decimal (e.g. 31, -67.421e-8, 83.456, 23.3e8) + hexadecimal (e.g. -0xa7b6p-7) + - bytes: + As a hex number (e.g. 0xab4b2f5e9d7f) + NOTE: Only multiple of 8 bits possible + - strings: + As a string without quotes. (e.g. ThisIsAString) + NOTE: currently strings may not contain ' ', ',' or ':' + - enum values: + As the enum value name + Enum values are tab-completed + - bools + true, false, 1, 0 + Bool values are tab-completed + - nested types: + The nested type fields enclosed in colons. E.g. target=:field1=1 field2=2 : + Nested types are tab-completed + NOTE: currently after each field value a space is required + - repeated fields: + A list of field values enclosed in colons, separated with a comma and a whitespace. + E.g. values=:2, 4, 16, 32: + Repeated fields are tab-completed + - map fields: + As a repeated field with nested key, value pairs (protobuf representation of maps). + E.g. my_map=::key=5 value=testval :, :key=40 value=anotherValue:: + Map fields are tab-completed + +OUTPUT_FORMAT: + This is a program written in a very simple formatting language. A valid program has + the following form: + @ (.)+ : ( || // ) : + Example: + ./gwhisper --customOutput @.listOfDevices:Found device /device_name/$'\n': 127.0.0.1 discoveryService.devices GetDevices + + Output: + Found device Mouse + Found device Keyboard + Found device Screen + ... + This will search for any number of fields with a name "listOfDevices" in the RPC reply message. For each match (multiple in case of repeated field), The string `"Found device /device_name/\n"` is printed with `/device_name/` replaced by the value of the field `"device_name"`. + + +EXAMPLES: + gWhisper exampledomain.org:50059 bakery orderCookies amount=5 + Connects to the gRPC server running at exampledomain.org on TCP port 50059 + and calls the orderCookies RPC method of the bakery service. + A request message will be passed to the RPC call with the 'amount' field + set to 5. Other fields, which might be present, are left at default values. + + gWhisper [2001:db8::2:1]:50059 bakery orderCookies config=:chocolate=true smarties=false : amount=5 + Uses IPv6 address and nested types in the request message. + + gWhisper 192.168.0.43 bakery orderCookies amount=0xa + Uses IPv4 address, default TCP port of 50051 and field value as hex. diff --git a/example.gif b/example.gif new file mode 100644 index 0000000..8f8c14c Binary files /dev/null and b/example.gif differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..11674eb --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,27 @@ +# Copyright 2019 IBM Corporation +# +# 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. + +cmake_minimum_required (VERSION 2.8) + +add_subdirectory(libArgParse) +add_subdirectory(libCli) +add_subdirectory(gwhisper) + +add_custom_target(compileCommands + COMMAND cp ${CMAKE_BINARY_DIR}/compile_commands.json ${CMAKE_SOURCE_DIR}/compile_commands.json + ) + +add_custom_target(versionDefine + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/generateVersionDefine.sh > ${CMAKE_CURRENT_BINARY_DIR}/versionDefine.h + ) diff --git a/src/generateVersionDefine.sh b/src/generateVersionDefine.sh new file mode 100755 index 0000000..0e09407 --- /dev/null +++ b/src/generateVersionDefine.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Copyright 2019 IBM Corporation +# +# 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. + +# get build date +DATE=`date +%Y-%m-%d_%H:%M:%S` + + +if [ -z "`command -v git`" ]; then + echo "$GWHISPER_BUILD_VERSION Build date: $DATE Build pwd: $PWD" + exit 0 +fi + +git status &> /dev/null +if [ $? -ne 0 ]; then + echo "$GWHISPER_BUILD_VERSION Build date: $DATE Build pwd: $PWD" + exit 0 +fi + +# get unique readable commit identification +gitId=$(git describe --tags) + +# check for uncommited changes +uncommitedChanges="" +git diff-index --quiet HEAD -- +if [ $? -eq 0 ]; then + uncommitedChanges="NoUncommitedChanges" +else + uncommitedChanges="UncommitedChanges" +fi + +# check for untracked files present +untrackedFiles="" +tmpUntracked=$(git ls-files --other --directory --exclude-standard) +if [ -z "$tmpUntracked" ]; then + untrackedFiles="NoUntrackedFiles" +else + untrackedFiles="UntrackedFiles" +fi + +versionString="$GWHISPER_BUILD_VERSION Git identifier: ${gitId} Build date: ${DATE} ${uncommitedChanges} ${untrackedFiles}" + +echo "#pragma once" +echo "#define GWHISPER_BUILD_VERSION \"$versionString\"" diff --git a/src/gwhisper/CMakeLists.txt b/src/gwhisper/CMakeLists.txt new file mode 100644 index 0000000..87d306e --- /dev/null +++ b/src/gwhisper/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright 2019 IBM Corporation +# +# 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. + +cmake_minimum_required (VERSION 2.8) + +set(TARGET_NAME "gwhisper") + +# this converts the help string into a raw string for use with --help +add_custom_command( + OUTPUT HelpString.h + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/generateRawStringFile.sh ${CMAKE_SOURCE_DIR}/doc/Usage.txt ${CMAKE_CURRENT_BINARY_DIR}/HelpString.h + DEPENDS ${CMAKE_SOURCE_DIR}/doc/Usage.txt + ) + +set(TARGET_SRC + gwhisper.cpp + HelpString.h + ) +add_executable(${TARGET_NAME} ${TARGET_SRC}) +target_link_libraries ( ${TARGET_NAME} + cli + ) + +add_dependencies(${TARGET_NAME} compileCommands versionDefine) diff --git a/src/gwhisper/generateRawStringFile.sh b/src/gwhisper/generateRawStringFile.sh new file mode 100755 index 0000000..001ae6e --- /dev/null +++ b/src/gwhisper/generateRawStringFile.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Copyright 2019 IBM Corporation +# +# 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. + +if [ "$#" -lt 2 ]; then + echo "Not enough arguments. Expected source and destination file" + exit 1 +fi +echo -n "R\"(" > $2 +cat $1 >>$2 +echo ")\"" >> $2 diff --git a/src/gwhisper/gwhisper.cpp b/src/gwhisper/gwhisper.cpp new file mode 100644 index 0000000..54e2f6c --- /dev/null +++ b/src/gwhisper/gwhisper.cpp @@ -0,0 +1,109 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include +#include +#include +#include // generated during build + +using namespace ArgParse; + +std::string getArgsAsString(int argc, char **argv) +{ + std::string result; + bool first = true; + for(int i = 1; i +; + +int main(int argc, char **argv) +{ + // First we construct the initial Grammar for the CLI tool: + Grammar grammarPool; + GrammarElement * grammarRoot = cli::constructGrammar(grammarPool); + std::string baseGrammar = grammarRoot->toString(); + + // Now we parse the given arguments using the grammar: + std::string args = getArgsAsString(argc, argv); + ParsedElement parseTree; + ParseRc rc = grammarRoot->parse(args.c_str(), parseTree); + + // TODO: add option to print parse tree after parsing: + // // Now we act according to the parse tree: + //std::cout << parseTree.getDebugString() << "\n"; + + if(parseTree.findFirstChild("DotExport") != "") + { + std::cout << grammarPool.getDotGraph(); + return 0; + } + + if(parseTree.findFirstChild("Complete") != "") + { + bool completeDebug = (parseTree.findFirstChild("CompleteDebug") != ""); + cli::printBashCompletions(rc.candidates, parseTree, args, completeDebug); + return 0; + } + + if(parseTree.findFirstChild("Version") != "") + { + std::cout << GWHISPER_BUILD_VERSION << std::endl; + return 0; + } + + if(parseTree.findFirstChild("Help") != "") + { + printf("%s", g_helpString); + return 0; + } + + if(rc.isGood() && (rc.lenParsedSuccessfully == args.length())) + { + return cli::call(parseTree); + } + + std::cout << "Parse failed. "; + std::cout << "Parsed until: '" << parseTree.getMatchedString() << "'" << std::endl; + //std::cout << parseTree.getDebugString() << "\n"; + if(rc. candidates.size() > 0) + { + std::cout << "Possible Candidates:"; + for(auto candidate : rc.candidates) + { + //printf("\nchoice:\n%s", candidate->getDebugString().c_str()); + printf("\n '%s'", candidate->getMatchedString().c_str()); + } + std::cout << std::endl; + } + + return -1; +} diff --git a/src/libArgParse/Alternation.hpp b/src/libArgParse/Alternation.hpp new file mode 100644 index 0000000..05e3af3 --- /dev/null +++ b/src/libArgParse/Alternation.hpp @@ -0,0 +1,158 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include + +namespace ArgParse +{ +class Alternation : public GrammarElement +{ + public: + Alternation(const std::string & f_elementName = ""): + GrammarElement("Alternation", f_elementName) + { + } + + virtual std::string toString() override + { + std::string result; + if(m_children.size()>1) + { + result += "("; + } + bool first = true; + for(auto child: m_children) + { + if(!first) + { + result += "||"; + } + else + { + first = false; + } + result += child->toString(); + } + if(m_children.size()>1) + { + result += ")"; + } + return result; + } + + virtual ParseRc parse(const char * f_string, ParsedElement & f_out_ParsedElement, size_t candidateDepth = 1, size_t startChild = 0) override + { + ParseRc rc; + f_out_ParsedElement.setGrammarElement(this); + + //std::cout << "Alternation " << std::to_string(m_instanceId) << ": parsing '" << f_string << "'\n"; + + std::vector > candidateList; + + std::shared_ptr winner; + std::shared_ptr maybeWinner; + GrammarElement * maybeWinnerGE; + size_t maybeCount = 0; + size_t newCandidateDepth = candidateDepth; + if(candidateDepth > 0) + { + // New idea: never allow forks by default. + // if we realize in the end, that we are unique, we parse + // again, this time allowing forks + newCandidateDepth--; + } + for(auto child : m_children) + { + auto newParsedElement = std::make_shared(&f_out_ParsedElement); + ParseRc childRc = child->parse(f_string, *newParsedElement, newCandidateDepth); + //std::cout << " Alternation pass1 "<< std::to_string(m_instanceId) << " parsed child ? rc=" << childRc.toString() << " #candidates: " << std::to_string(childRc.candidates.size()) << std::endl; + if(childRc.isGood()) + { + if(rc.lenParsedSuccessfully <= childRc.lenParsedSuccessfully) + { + // FIXME: what about optional childs? They always succeed but have candidates + // -> this is low prio, as optional childs in alternations do not really make sense, but should be supported in the end anyways + rc.lenParsedSuccessfully = childRc.lenParsedSuccessfully; + rc.lenParsed = childRc.lenParsed; + winner = newParsedElement; + } + } + if((not childRc.isGood()) && (childRc.errorType != ParseRc::ErrorType::unexpectedText)) + { + maybeWinner = newParsedElement; + maybeWinnerGE = child; + maybeCount++; + + // this is a candidate for completion + //std::cout << " Alt"<< std::to_string(m_instanceId) << "one child rc=" << childRc.toString() << std::endl; + for(auto candidate : childRc.candidates) + { + //std::cout << " Alternation"<< std::to_string(m_instanceId) << ": have possible candidate: '" << candidate->getMatchedString() << "'" << std::endl; + candidateList.push_back(candidate); + } + } + } + + if(winner != nullptr) + { + f_out_ParsedElement.addChild(winner); + } + else + { + if(maybeCount == 1) + { + // we have had enough text to uniquely select one of the alternates + // => we can add it to the f_out_ParsedElement: + f_out_ParsedElement.addChild(maybeWinner); + // in this case we could uniquely identify a candidate :) + // so we need to parse again for candidates, this time allowing for forks + candidateList.clear(); + ParsedElement unused; + ParseRc childRc = maybeWinnerGE->parse(f_string, unused, candidateDepth); + candidateList = childRc.candidates; + //std::cout << " Alternation pass2 "<< std::to_string(m_instanceId) << " parsed child ? rc=" << childRc.toString() << " #candidates: " << std::to_string(childRc.candidates.size()) << std::endl; + } + + // merge RCs + if(m_children.size() == 0) + { + rc.errorType = ParseRc::ErrorType::success; + } + else if(candidateList.size() == 0) + { + rc.errorType = ParseRc::ErrorType::unexpectedText; + } + else + { + rc.errorType = ParseRc::ErrorType::missingText; + rc.lenParsed = strlen(f_string); + } + } + + // add all candidates to the candidate list + for(auto candidate : candidateList) + { + //std::cout << "Alt " << std::to_string(m_instanceId) << " handling candidate '" << candidate->getMatchedString() << "'" << std::endl; + auto candidateRoot = std::make_shared(f_out_ParsedElement.getParent()); + candidateRoot->setGrammarElement(this); + candidateRoot->addChild(candidate); + rc.candidates.push_back(candidateRoot); + } + + return rc; + } +}; + +} diff --git a/src/libArgParse/ArgParse.cpp b/src/libArgParse/ArgParse.cpp new file mode 100644 index 0000000..ddcae00 --- /dev/null +++ b/src/libArgParse/ArgParse.cpp @@ -0,0 +1,106 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +//uint32_t ArgParse::GrammarElement::m_instanceCounter = 0; + +std::string ArgParse::ParsedElement::getDebugString(std::string f_prefix) +{ + std::string result; + if(m_grammarElement == nullptr) + { + return "!!Uninitialized Element!!"; + } + result += f_prefix + m_grammarElement->getTypeName() + "(" + m_grammarElement->getElementName() + "): \"" + getMatchedString() + "\" (" + std::string(m_stops ? "stopped" : "alive") + ")\n"; + for(auto child : m_children) + { + result += child->getDebugString(f_prefix + " "); + } + return result; +} + +std::string ArgParse::ParsedElement::findFirstChild(const std::string & f_elementName) +{ + bool found = false; + ParsedElement & result = findFirstSubTree(f_elementName, found); + if(found) + { + return result.getMatchedString(); + } + else + { + return ""; + } +} + +void ArgParse::ParsedElement::findAllSubTrees(const std::string & f_elementName, std::vector & f_out_result, bool f_doNotSearchChildsOfMatchingElements) +{ + bool match = false; + if(m_grammarElement->getElementName() == f_elementName) + { + match = true; + f_out_result.push_back(this); + if(f_doNotSearchChildsOfMatchingElements) + { + return; + } + } + + for(auto child : m_children) + { + child->findAllSubTrees(f_elementName, f_out_result); + } +} + +ArgParse::ParsedElement & ArgParse::ParsedElement::findFirstSubTree(const std::string & f_elementName, bool & f_out_found) +{ + //std::cout << "searching for " << f_elementName << " going through " << m_grammarElement->getTypeName() << " " << m_grammarElement->getElementName() << std::endl; + if(m_grammarElement->getElementName() == f_elementName) + { + f_out_found = true; + //std::cout << "searched for " << f_elementName << " returning true\n"; + return *this; + } + else + { + for(auto child : m_children) + { + bool found = false; + ParsedElement & result = child->findFirstSubTree(f_elementName, found); + if(found) + { + f_out_found = true; + return result; + } + } + } + + f_out_found = false; + return *this; +} + +std::string ArgParse::Grammar::getDotGraph() +{ + //std::cout << "GENERATING DOT GRAPH\n"; + std::string result = "digraph {\n"; + result += "ordering=out;\n"; + for(auto & node : m_nodes) + { + //printf("getting info for node %p\n", node.get()); + result += node->getDotNode(); + } + result += "}\n"; + return result; +} diff --git a/src/libArgParse/ArgParse.hpp b/src/libArgParse/ArgParse.hpp new file mode 100644 index 0000000..d974888 --- /dev/null +++ b/src/libArgParse/ArgParse.hpp @@ -0,0 +1,57 @@ +// Copyright 2019 IBM Corporation +// +// 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. + + +/// This is a summary header file for a group of classes which compose an argument parsing system. +/// The high-level concept of this framework is as follows: +/// - GrammarElement instances are created from a memory pool and compose a graph representing the grammar to parse. +/// - Multiple derivations of GrammarElement implement different gramar features (Alternation, Concatenation, FixedString, Optional, RegEx, ...) +/// Those elements provide the building blocks for the grammar to be implemented by the user. +/// - GrammarElements may be combined by calling the addChild() methods. +/// - GrammarElements may be associated with a string tag, which will be assigned to all elements whcih get parsed by this element. (similar to backreferences in regex) +/// - Once the Grammar is constructed, the user may call parse() with a given string on one of the GramarElements (typically the root/start element) +/// - The parse() function will generate +/// - a parse tree containing all parsed values, composed of ParsedElement instances +/// - a list of possible parse trees results if parse was incomplete. Those may be used for completion. +/// - additional meta information about the parse (return code, parsed length, etc.) +/// - The parse tree provides utility functions to access parsed data: +/// - search of tagged elements +/// - interation/inspection of the tree +/// - Each ParsedElement contains a reference to its associated GrammarElement. + +// TODO: individual classes and their member functions of this framework are not yet documented well enough + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + diff --git a/src/libArgParse/ArgParseUtils.hpp b/src/libArgParse/ArgParseUtils.hpp new file mode 100644 index 0000000..65369e6 --- /dev/null +++ b/src/libArgParse/ArgParseUtils.hpp @@ -0,0 +1,67 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include +#include +#include +#include + +namespace ArgParse +{ +struct ParseRc +{ + enum class ErrorType + { + success, + missingText, + unexpectedText, + }; + + ErrorType errorType = ErrorType::success; + size_t lenParsedSuccessfully = 0; + size_t lenParsed = 0; + + std::string toString() + { + switch(errorType) + { + case ErrorType::success: + return "success"; + break; + case ErrorType::missingText: + return "missingText"; + break; + case ErrorType::unexpectedText: + return "unexpectedText"; + break; + default: + return "???"; + break; + } + } + + bool isGood() + { + return errorType == ErrorType::success; + } + + bool isBad() + { + return not isGood(); + } + + std::vector > candidates; +}; +} diff --git a/src/libArgParse/CMakeLists.txt b/src/libArgParse/CMakeLists.txt new file mode 100644 index 0000000..52a2ccf --- /dev/null +++ b/src/libArgParse/CMakeLists.txt @@ -0,0 +1,27 @@ +# Copyright 2019 IBM Corporation +# +# 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. + +cmake_minimum_required (VERSION 2.8) + +set (CMAKE_CXX_STANDARD 11) + +set(TARGET_NAME "ArgParse") +set(TARGET_SRC + ArgParse.cpp + ) +add_library(${TARGET_NAME} ${TARGET_SRC}) + +target_link_libraries(${TARGET_NAME} + #boost_regex + ) diff --git a/src/libArgParse/Concatenation.hpp b/src/libArgParse/Concatenation.hpp new file mode 100644 index 0000000..3b11ed0 --- /dev/null +++ b/src/libArgParse/Concatenation.hpp @@ -0,0 +1,164 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include +namespace ArgParse +{ +class Concatenation : public GrammarElement +{ + public: + Concatenation(const std::string & f_elementName = "") : + GrammarElement("Concatenation", f_elementName) + { + } + + virtual std::string toString() override + { + std::string result; + if(m_children.size()>1) + { + result += "("; + } + //bool first = true; + for(auto child: m_children) + { + //if(!first) + //{ + // result += " "; + //} + //else + //{ + // first = false; + //} + result += child->toString(); + } + if(m_children.size()>1) + { + result += ")"; + } + return result; + } + + virtual ParseRc parse(const char * f_string, ParsedElement & f_out_ParsedElement, size_t candidateDepth = 1, size_t startChild = 0) override + { + //std::cout << "Concat " << std::to_string(m_instanceId) << " parsing '" << std::string(f_string) << "' cd=" << std::to_string(candidateDepth) << std::endl; + ParseRc rc; + ParseRc childRc; + f_out_ParsedElement.setGrammarElement(this); + + + //std::cout << "Concat "<< std::to_string(m_instanceId) << " parsing '" << std::string(f_string) << "' starting with child " << std::to_string(startChild) << "/" << std::to_string(m_children.size()-1)<< "' cd=" << std::to_string(candidateDepth) << std::endl; + for(size_t i = startChild; (i(&f_out_ParsedElement); + childRc = child->parse(&f_string[rc.lenParsed], *newParsedElement); + //std::cout << " Concat "<< std::to_string(m_instanceId) << " parsed child" << std::to_string(i) << " rc=" << childRc.toString() << " #candidates: " << std::to_string(childRc.candidates.size()) << std::endl; + rc.lenParsed += childRc.lenParsed; + rc.lenParsedSuccessfully += childRc.lenParsedSuccessfully; + + // merge RCs + rc.errorType = childRc.errorType; + + if(childRc.candidates.size() > 0) + { + for(auto candidate : childRc.candidates) + { + //std::cout << "Concat " << std::to_string(m_instanceId) << " handling candidate from child " << std::to_string(i)<< " '" << candidate->getMatchedString() << "' cd=" << std::to_string(candidateDepth) << std::endl; + // we create a new candidate (same tree level as f_out_ParsedElement) + auto candidateRoot = std::make_shared(f_out_ParsedElement.getParent()); + candidateRoot->setGrammarElement(this); + + // first add all previous childs to the new root (from concatenation before the failing element): + for(auto oldChild : f_out_ParsedElement.getChildren()) + { + candidateRoot->getChildren().push_back(oldChild); + } + + // add the candidate to the new root: + candidateRoot->addChild(candidate); + + // decide on future recursion depth: + size_t newCandidateDepth = candidateDepth; + if(childRc.candidates.size() > 1) + { + // if we have multiple candidates, we reduce the candidateDepth by 1 + newCandidateDepth--; + } + + // we only go on parsing the next childs, if we have only one candidate (unique follow up possible) + // or we still are allowed to fork + // Otherwise we just do not return ANY candidates + if(candidateDepth > 0 || childRc.candidates.size() == 1) + { + // Now continue parsing the other childs, using this candidate as a base: + ParseRc candidateRc; + if(!candidateRoot->isStopped()) + { + //std::cout << " -> forking for this candidate" << std::endl; + candidateRc = parse("", *candidateRoot, newCandidateDepth, i+1); + } + if(candidateRc.candidates.size() == 0) + { + // if we could not find any more candidates, we add this candidate: + candidateRoot->setStops(); //If we could not find a candidate we need to stop compeleting TODO: write unit test to ensure this + rc.candidates.push_back(candidateRoot); + //std::cout << "Concat " << std::to_string(m_instanceId) << " 0push candidate '" << candidateRoot->getMatchedString() << "'" << std::endl; + } + else if(candidateRc.candidates.size() == 1) + { + rc.candidates.push_back(candidateRc.candidates[0]); + //std::cout << "Concat " << std::to_string(m_instanceId) << " 1push candidate '" << candidateRc.candidates[0]->getMatchedString() << "'" << std::endl; + } + else + { + // if we could find more candidates in the recursion, we add them instead :) + // but only if we are still allowed to + for(auto cnd : candidateRc.candidates) + { + rc.candidates.push_back(cnd); + //std::cout << "Concat " << std::to_string(m_instanceId) << " Mpush candidate '" << candidateRc.candidates[0]->getMatchedString() << "'" << std::endl; + } + } + } + } + + } + + if(childRc.isGood() || childRc.errorType == ParseRc::ErrorType::missingText) + { + //std::cout << "Concat "<< std::to_string(m_instanceId) << " have good child " << std::to_string(i) << " matched string: " << newParsedElement->getMatchedString() << std::endl; + // need to do this AFTER candidate avaluation, as some parse + // methods will return good rc but still have candidates (e.g. repetition) + // in this case we whould add their results to their candidates again + f_out_ParsedElement.addChild(newParsedElement); + if(not childRc.isGood()) + { + // TODO: add this for other GrammarElements too + // TODO: write unit test for this! + f_out_ParsedElement.setIncompleteParse(); + } + } + } + + //std::cout << "Concat "<< std::to_string(m_instanceId) << " returning rc=" << rc.toString() << std::endl; + + return rc; + } +}; + +} diff --git a/src/libArgParse/FixedString.hpp b/src/libArgParse/FixedString.hpp new file mode 100644 index 0000000..02aa712 --- /dev/null +++ b/src/libArgParse/FixedString.hpp @@ -0,0 +1,86 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include + +namespace ArgParse +{ +class FixedString : public GrammarElement +{ + public: + FixedString(const std::string & f_string, const std::string & f_elementName = "") : + GrammarElement("FixedString", f_elementName), + m_string(f_string) + { + } + + virtual std::string toString() override + { + return m_string; + } + + virtual ParseRc parse(const char * f_string, ParsedElement & f_out_ParsedElement, size_t candidateDepth = 1, size_t startChild = 0) override + { + ParseRc rc; + ParseRc childRc; + f_out_ParsedElement.setGrammarElement(this); + + //std::cout << " FixedString"<< std::to_string(m_instanceId) << "parsing '" << std::string(f_string) << "'" << std::endl; + //printf("comparing: '%s' == '%s'\n", f_string, m_string.c_str()); + if(strncmp(m_string.c_str(), f_string, m_string.size()) == 0) + { + //printf(" -> same\n"); + rc.errorType = ParseRc::ErrorType::success; + rc.lenParsedSuccessfully = m_string.size(); + rc.lenParsed = m_string.size(); + f_out_ParsedElement.setMatchedString(m_string); + } + else + { + rc.lenParsedSuccessfully = 0; + if(strncmp(m_string.c_str(), f_string, strlen(f_string)) == 0) + { + rc.lenParsed = strlen(f_string); + // have a candidate for completion :) + //printf(" -> completion possible\n"); + // create a candidate: + auto candidate = std::make_shared(&f_out_ParsedElement); + candidate->setGrammarElement(this); + candidate->setMatchedString(m_string); + rc.candidates.push_back(candidate); + + // set rc + rc.errorType = ParseRc::ErrorType::missingText; + } + else + { + //printf(" -> totally different\n"); + rc.errorType = ParseRc::ErrorType::unexpectedText; + } + } + //std::cout << " FixedString"<< std::to_string(m_instanceId) << " rc=" << rc.toString() << std::endl; + return rc; + } + virtual std::string getDotNode() override + { + std::string result = ""; + result += "n" + std::to_string(m_instanceId) + "[label=\"" + std::to_string(m_instanceId) + " " + m_typeName + " " + m_elementName + " " + m_tag + "'" + m_string + "'\"];\n"; + return result; + } + private: + const std::string m_string; +}; + +} diff --git a/src/libArgParse/Grammar.hpp b/src/libArgParse/Grammar.hpp new file mode 100644 index 0000000..bfd8304 --- /dev/null +++ b/src/libArgParse/Grammar.hpp @@ -0,0 +1,49 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +namespace ArgParse +{ + + class Grammar + { + public: + template + T * createElement( Args&&... f_args ) + { + T * newNodePtr = new T(std::forward(f_args)...); + std::unique_ptr newNode(newNodePtr); + m_nodes.push_back(std::move(newNode)); + return newNodePtr; + } + + void setRoot( GrammarElement * f_rootElement) + { + m_rootElement = f_rootElement; + } + + std::string getDotGraph(); + + virtual ~Grammar() + { + } + + private: + std::vector< std::unique_ptr > m_nodes; + GrammarElement * m_rootElement; + }; + +} + diff --git a/src/libArgParse/GrammarElement.hpp b/src/libArgParse/GrammarElement.hpp new file mode 100644 index 0000000..704284e --- /dev/null +++ b/src/libArgParse/GrammarElement.hpp @@ -0,0 +1,148 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include +#include +#include +#include +#include + +namespace ArgParse +{ +// a Graph +class GrammarElement; + +class GrammarElement +{ + public: + GrammarElement(std::string f_typeName, std::string f_elementName = "") : + m_typeName(f_typeName), + m_elementName(f_elementName), + m_instanceId(getAndIncrementInstanceCounter()) + { + } + + /* Parses a given string into a parse tree. + * @param f_string string to be parsed (null terminated cstring) + * @param f_out_ParsedElement Reference to a ParsedElement, which will + * be filled with the parse result (parse tree). + * i.e. this Grammar element will be parsed into the given ParsedElement f_out_ParsedElement. + * The f_out_ParsedElement will always reference this GrammarElement + * instance after it is passed to this method. + * If the parse method could uniquely identify or parse a child, the + * f_out_ParsedElement object will also have a child referencing to the + * child-GrammarElement. + * @param candidateDepth This parameter controls the maximum recursion/fork + * depth when analyzing candidates. + * This means, that when a Element could continue parsing with X1 > 1 candidates, + * Parsing is tried for each of these candidates, but candidateDepth is reduced by 1. + * If those candidates return X2 sub-candidates the following will happen: + * If X2 > 1 and the candidateDepth is zero, parsing will stop and the + * returned candidates are ignored. + * If X2 > 1 and the candidateDepth is > 0, parsing is done for all + * those childs with a candidateDepth reduced by 1 again. + * If X2 == 1 parsing will continue + * NOTE: This description describes the behavior of Concatenations as an example + * Behavior for other Elements is similar, but might differ slightly. + * For example Alternations always continues parsing + * (think: An Alternation with an alternation as a child is + * semantically the same as a single Alternation with merged childs) + * Currently only a candidateDepth of 1 is tested. + * @param startChild The child number to start parsing from. Can be + * used to start parsing from a specific gramar element. + * Currently this is mostly used internally. + * TODO: Think about making this protected + * @returns ParseRc with the following attributes: + * candidate list containing a list of ParsedElements which + * could be used in place of f_out_ParsedElement. + */ + virtual ParseRc parse(const char * f_string, ParsedElement & f_out_ParsedElement, size_t candidateDepth = 1, size_t startChild = 0) = 0; + + virtual std::string toString() + { + std::string result; + result = m_typeName + "(" + m_elementName + ":" + std::to_string(m_instanceId) + ")" + "{"; + for(auto child: m_children) + { + result += child->toString(); + result += ", "; + } + result += "}"; + return result; + } + + GrammarElement * addChild(GrammarElement * f_child) + { + f_child->setParent(this); + m_children.push_back(f_child); + return this; + } + + void setParent(GrammarElement * f_parent) + { + m_parent = f_parent; + } + + // TODO: what is the difference between tag and ElementName?? + // tag does not seem to be used + std::string getTag() const + { + return m_tag; + } + + std::string getTypeName() const + { + return m_typeName; + } + + std::string getElementName() const + { + return m_elementName; + } + + virtual ~GrammarElement() + { + + } + + virtual std::string getDotNode() + { + std::string result = ""; + result += "n" + std::to_string(m_instanceId) + "[label=\"" + std::to_string(m_instanceId) + " " + m_typeName + " " + m_elementName + " " + m_tag + "\"];\n"; + for(auto child : m_children) + { + result += " n" + std::to_string(m_instanceId) + " -> n" + std::to_string(child->m_instanceId) + ";\n"; + } + //std::cout << " got dot string: " << result; + return result; + } + protected: + GrammarElement * m_parent; + std::vector< GrammarElement * > m_children; + + std::string m_tag; + const std::string m_typeName; + const std::string m_elementName; + const uint32_t m_instanceId; + private: + static uint32_t getAndIncrementInstanceCounter() + { + static uint32_t instanceCounter = 0; + return instanceCounter++; + } +}; + + +} diff --git a/src/libArgParse/GrammarInjector.hpp b/src/libArgParse/GrammarInjector.hpp new file mode 100644 index 0000000..9605684 --- /dev/null +++ b/src/libArgParse/GrammarInjector.hpp @@ -0,0 +1,74 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include +#include + +namespace ArgParse +{ + +class GrammarInjector : public GrammarElement +{ + public: + GrammarInjector(std::string f_typeName, const std::string & f_elementName = "") : + GrammarElement("GrammarInjector::" + f_typeName, f_elementName) + { + } + virtual ParseRc parse(const char * f_string, ParsedElement & f_out_ParsedElement, size_t candidateDepth = 1, size_t startChild = 0) override final + { + if(m_children.size() == 0) + { + // we first need to inject new grammar: + addChild(getGrammar(f_out_ParsedElement.getRoot())); + } + + f_out_ParsedElement.setGrammarElement(this); + auto child = std::make_shared(&f_out_ParsedElement); + // we transparently skip to parsing the new child + //return m_children[0]->parse(f_string, f_out_ParsedElement, candidateDepth); + ParseRc childRc = m_children[0]->parse(f_string, *child, candidateDepth); + + f_out_ParsedElement.addChild(child); + + return childRc; + } + + virtual GrammarElement * getGrammar(ParsedElement * f_parseTree) = 0; +}; + + +class GrammarInjectorTest : public GrammarInjector +{ + public: + GrammarInjectorTest(Grammar & f_grammar) : + GrammarInjector("Test"), + m_grammar(f_grammar) + { + } + + virtual GrammarElement * getGrammar(ParsedElement * f_parseTree) override + { + auto result = m_grammar.createElement(); + result->addChild(m_grammar.createElement("inject1")); + result->addChild(m_grammar.createElement("inject2")); + return result; + }; + + private: + Grammar & m_grammar; + +}; + +} diff --git a/src/libArgParse/Optional.hpp b/src/libArgParse/Optional.hpp new file mode 100644 index 0000000..5d6f8d7 --- /dev/null +++ b/src/libArgParse/Optional.hpp @@ -0,0 +1,83 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include + +namespace ArgParse +{ + +class Optional : public GrammarElement +{ + public: + Optional(const std::string & f_elementName = "") : + GrammarElement("Optional", f_elementName) + { + } + + virtual std::string toString() override + { + std::string result; + result += "["; + for(auto child: m_children) + { + result += child->toString(); + } + result += "]"; + return result; + } + + virtual ParseRc parse(const char * f_string, ParsedElement & f_out_ParsedElement, size_t candidateDepth = 1, size_t startChild = 0) override + { + ParseRc rc; + ParseRc childRc; + f_out_ParsedElement.setGrammarElement(this); + + GrammarElement * candidate = nullptr; + ParseRc candidateRc; + + auto child = m_children[0]; // FIXME: range check + auto newParsedElement = std::make_shared(&f_out_ParsedElement); + //printf("Optional start parse\n"); + childRc = child->parse(&f_string[rc.lenParsedSuccessfully], *newParsedElement); + //printf("Optional parse RC: "); + //childRc.print(); + //printf("\n"); + if(childRc.isGood()) + { + rc.lenParsedSuccessfully += childRc.lenParsedSuccessfully; + rc.lenParsed += childRc.lenParsed; + f_out_ParsedElement.addChild(newParsedElement); + } + if((not childRc.isGood()) && (childRc.errorType != ParseRc::ErrorType::unexpectedText)) + { + // add all candidates resulting from the child: + for(auto candidate : childRc.candidates) + { + //printf("add optional candidate : '%s'\n", candidate->getMatchedString().c_str()); + auto realCandidate = std::make_shared(f_out_ParsedElement.getParent()); + realCandidate->setGrammarElement(this); + realCandidate->setStops(); + realCandidate->addChild(candidate); + rc.candidates.push_back(realCandidate); + } + } + + rc.errorType = ParseRc::ErrorType::success; + + return rc; + } +}; + +} diff --git a/src/libArgParse/ParsedElement.hpp b/src/libArgParse/ParsedElement.hpp new file mode 100644 index 0000000..04049fd --- /dev/null +++ b/src/libArgParse/ParsedElement.hpp @@ -0,0 +1,180 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include +#include +#include +#include + +namespace ArgParse +{ +class GrammarElement; +// a Tree +class ParsedElement +{ + public: + ParsedElement() : + m_grammarElement(nullptr), + m_parent(this) + { + } + + ParsedElement(ParsedElement * f_parent) : + m_grammarElement(nullptr), + m_parent(f_parent) + { + } + + ParsedElement(GrammarElement * f_grammarElement) : + m_grammarElement(f_grammarElement), + m_parent(this) + { + } + + GrammarElement * getGrammarElement() + { + return m_grammarElement; + } + + void setGrammarElement(GrammarElement * f_grammarElement) + { + m_grammarElement = f_grammarElement; + } + + ParsedElement & addChild(std::shared_ptr f_element) + { + f_element->setParent(this); + m_children.push_back(f_element); + return *(m_children.back()); + } + + void setMatchedString(const std::string & f_string) + { + m_matchedString = f_string; + } + + // prints the "flattened parse tree" i.e. the complete matched string. + std::string getMatchedString() const + { + std::string result = m_matchedString; + for(auto child : m_children) + { + result += child->getMatchedString(); + } + return result; + } + + // prints out the complete parse tree. + std::string getDebugString(std::string f_prefix = ""); + + std::vector > & getChildren() + { + return m_children; + } + + // depth first search for a single element, directly returning the matched string. + // @param f_elementName element name to search for (inherited from grammar element) + std::string findFirstChild(const std::string & f_elementName); + + // depth first search for a single element. + // @param f_elementName element name to search for (inherited from grammar element) + ParsedElement & findFirstSubTree(const std::string & f_elementName, bool & f_out_found); + + // depth first search for elements. + // @param f_elementName element name to search for (inherited from grammar element) + // @param f_out_result vector to which found elements are written to + // @param f_doNotSearchChildsOfMatchingElements if true, the search will + // not traverse deeper in a branch after finding the first match. + // example: . + // 1.A -> 2.B -> 3.B + // -> 4.C -> 5.B + // -> 6.D + // -> 7.B + // search for B with f_doNotSearchChildsOfMatchingElements == true: + // 2, 5, 7 + // search for B with f_doNotSearchChildsOfMatchingElements == false: + // 2, 3, 5, 7 + void findAllSubTrees(const std::string & f_elementName, std::vector & f_out_result, bool f_doNotSearchChildsOfMatchingElements = false); + + void setParent(ParsedElement * f_parent) + { + m_parent = f_parent; + } + + ParsedElement * getParent() + { + return m_parent; + } + + ParsedElement * getRoot() + { + ParsedElement * result = getParent(); + while(result->getParent() != result) + { + result = result->getParent(); + } + + return result; + } + + void setStops() + { + if(m_children.size() == 0) + { + m_stops = true; + } + else + { + + m_children.back()->setStops(); + } + } + bool isStopped() + { + if(m_stops) + { + return true; + } + for(auto child :m_children) + { + bool stopped = child->isStopped(); + if(stopped) + { + return true; + } + } + return false; + } + + void setIncompleteParse() + { + m_incompleteParse = true; + } + + bool isCompletelyParsed() + { + return (not m_incompleteParse); + } + + private: + GrammarElement * m_grammarElement; + ParsedElement * m_parent; + std::vector< std::shared_ptr > m_children; + bool m_stops = false; + std::string m_matchedString; + bool m_incompleteParse = false; +}; + +} diff --git a/src/libArgParse/RegEx.hpp b/src/libArgParse/RegEx.hpp new file mode 100644 index 0000000..d89d501 --- /dev/null +++ b/src/libArgParse/RegEx.hpp @@ -0,0 +1,127 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include + +#ifdef BUILD_CONFIG_USE_BOOST_REGEX + #include +#else + #include +#endif + +namespace ArgParse +{ + +// we use our own regex namespace here in which we provide regex types. +// Depending on build configuration these are either boost or standard library +// regex implementations +namespace regex +{ + #ifdef BUILD_CONFIG_USE_BOOST_REGEX + using boost::cmatch; + using boost::regex_search; + using boost::regex; + #else + using std::cmatch; + using std::regex_search; + using std::regex; + #endif +} + +class RegEx : public GrammarElement +{ + public: + + RegEx(const std::string f_regEx, std::string f_elementName = "") : + GrammarElement("RegEx", f_elementName), + m_regEx(f_regEx), + m_regExString(f_regEx) + { + } + + virtual std::string toString() override + { + std::string result; + if(m_elementName != "") + { + result = "/" + m_elementName + ":" + std::to_string(m_instanceId) + "/"; + } + else if(m_regExString != "") + { + result += "/"; + result += m_regExString; + result += "/"; + + } + else + { + result = m_typeName + "(UnnamedRegex:" + std::to_string(m_instanceId) + ")"; + } + return result; + } + + virtual ParseRc parse(const char * f_string, ParsedElement & f_out_ParsedElement, size_t candidateDepth = 1, size_t startChild = 0) override + { + ParseRc rc; + ParseRc childRc; + f_out_ParsedElement.setGrammarElement(this); + + //std::cmatch match; + regex::cmatch match; + if( + //std::regex_search(f_string, match, m_regEx) + regex::regex_search(f_string, match, m_regEx) + and + (match.position() == 0) + ) + { + // match has to be at the beginning + rc.errorType = ParseRc::ErrorType::success; + rc.lenParsedSuccessfully = match.length(); + rc.lenParsed = match.length(); + f_out_ParsedElement.setMatchedString(match[0]); + //printf("regex %u /%s/ did match\n", m_instanceId, m_regExString.c_str()); + } + else + { + //printf("regex %u /%s/ did not match\n", m_instanceId, m_regExString.c_str()); + rc.lenParsedSuccessfully = 0; + rc.lenParsed = strlen(f_string); + if(strlen(f_string) == 0) + { + //printf("regex %u /%s/ have missing text\n", m_instanceId, m_regExString.c_str()); + rc.errorType = ParseRc::ErrorType::missingText; + } + else + { + rc.errorType = ParseRc::ErrorType::unexpectedText; + } + } + + return rc; + } + virtual std::string getDotNode() override + { + std::string result = ""; + result += "n" + std::to_string(m_instanceId) + "[label=\"" + std::to_string(m_instanceId) + " " + m_typeName + " " + m_elementName + " " + m_tag + "'" + m_regExString + "'\"];\n"; + return result; + } + private: + //const std::regex m_regEx; + const regex::regex m_regEx; + const std::string m_regExString; +}; + +} diff --git a/src/libArgParse/Repetition.hpp b/src/libArgParse/Repetition.hpp new file mode 100644 index 0000000..846d7d9 --- /dev/null +++ b/src/libArgParse/Repetition.hpp @@ -0,0 +1,126 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include + +namespace ArgParse +{ +class Repetition : public GrammarElement +{ + public: + Repetition(const std::string & f_elementName = ""): + GrammarElement("Repetition", f_elementName) + { + } + + virtual std::string toString() override + { + std::string result; + result += "("; + result += m_children[0]->toString(); + result += ")*"; + return result; + } + + virtual ParseRc parse(const char * f_string, ParsedElement & f_out_ParsedElement, size_t candidateDepth = 1, size_t startChild = 0) override + { + //printf("rep parse\n"); + ParseRc rc; + ParseRc childRc; + f_out_ParsedElement.setGrammarElement(this); + + GrammarElement * candidate = nullptr; + ParseRc candidateRc; + + GrammarElement * child = nullptr; + if(m_children.size() > 0) + { + child = m_children[0]; + } + std::vector> successfullyParsedChilds; + bool overParsed = false; + while(childRc.isGood() && (child != nullptr) ) + { + if(childRc.isGood() && (f_string[rc.lenParsedSuccessfully] == '\0')) + { + // we parsed successfullt and exactly aligned with the end :) + // we continue parsing once more, to get possible further candidates + // then we end + // we set this flag here, to remember to switch the RC to success + overParsed = true; + } + auto newParsedElement = std::make_shared(&f_out_ParsedElement); + //printf("Optional start parse\n"); + childRc = child->parse(&f_string[rc.lenParsedSuccessfully], *newParsedElement); + //std::cout << " Rep "<< std::to_string(m_instanceId) << " parsed child. rc=" << childRc.toString() << std::endl; + //printf("Optional parse RC: "); + //childRc.print(); + //printf("\n"); + if(childRc.isGood() || childRc.errorType == ParseRc::ErrorType::missingText) + { + rc.lenParsedSuccessfully += childRc.lenParsedSuccessfully; + rc.lenParsed += childRc.lenParsed; + f_out_ParsedElement.addChild(newParsedElement); + } + if(childRc.isGood()) + { + // TODO: make unittest where those lists are different (partial parse should only complete partial result, but not append to successful list completions) + successfullyParsedChilds.push_back(newParsedElement); + } + } + // FIXME: maybe we have to move this in the loop and also process good rcs here with candidates (from optional/repetition/etc.) similarly to concat + if((not childRc.isGood()) && (childRc.errorType != ParseRc::ErrorType::unexpectedText)) + { + // add all candidates resulting from the child: + for(auto candidate : childRc.candidates) + { + //std::cout << "Rep " << std::to_string(m_instanceId) << " handling candidate '" << candidate->getMatchedString() << "'" << std::endl; + //printf("add optional candidate : '%s'\n", candidate->getMatchedString().c_str()); + auto realCandidate = std::make_shared(f_out_ParsedElement.getParent()); + realCandidate->setGrammarElement(this); + realCandidate->setStops(); // think about this is this required for repetition? + // add all previous childs (similar to concatenation): + for(auto previousChild : successfullyParsedChilds) + { + realCandidate->addChild(previousChild); + } + realCandidate->addChild(candidate); + //std::cout << " Rep "<< std::to_string(m_instanceId) << " add candidate '" << realCandidate->getMatchedString() << "'" << std::endl; + rc.candidates.push_back(realCandidate); + } + if(overParsed) + { + // if we are at the end of the string we return no error + // -> why?? + rc.errorType = ParseRc::ErrorType::success; + } + else + { + // otherwise we still have missing text + rc.errorType = ParseRc::ErrorType::missingText; + } + } + else + { + // if we saw something completely wrong, we declare us as success (we also allow zero matches) + rc.errorType = ParseRc::ErrorType::success; + } + + // FIXME empty string is also a candidate!! + return rc; + } +}; + +} diff --git a/src/libArgParse/WhiteSpace.hpp b/src/libArgParse/WhiteSpace.hpp new file mode 100644 index 0000000..730541c --- /dev/null +++ b/src/libArgParse/WhiteSpace.hpp @@ -0,0 +1,88 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include +namespace ArgParse +{ +class WhiteSpace : public GrammarElement +{ + public: + WhiteSpace() : + GrammarElement("WhiteSpace") + { + } + + virtual std::string toString() override + { + return " "; + } + + virtual ParseRc parse(const char * f_string, ParsedElement & f_out_ParsedElement, size_t candidateDepth = 1, size_t startChild = 0) override + { + ParseRc rc; + ParseRc childRc; + f_out_ParsedElement.setGrammarElement(this); + + std::string matchedString = ""; + size_t i; + for(i = 0; i< strlen(f_string); i++) + { + if(f_string[i] == ' ') + { + matchedString += f_string[i]; + } + else + { + break; + } + } + //printf("comparing: '%s' == '%s'\n", f_string, m_string.c_str()); + if(matchedString != "") + { + //printf(" -> same\n"); + rc.errorType = ParseRc::ErrorType::success; + rc.lenParsedSuccessfully = i; + rc.lenParsed = i; + f_out_ParsedElement.setMatchedString(matchedString); + } + else + { + rc.lenParsedSuccessfully = 0; + if(i == strlen(f_string)) + { + rc.lenParsed = strlen(f_string); + // have a candidate for completion :) + //printf(" -> completion possible\n"); + // create a candidate: + auto candidate = std::make_shared(&f_out_ParsedElement); + candidate->setGrammarElement(this); + candidate->setMatchedString(" "); + rc.candidates.push_back(candidate); + + // set rc + rc.errorType = ParseRc::ErrorType::missingText; + } + else + { + //printf(" -> totally different\n"); + rc.errorType = ParseRc::ErrorType::unexpectedText; + } + } + + return rc; + } +}; + +} diff --git a/src/libCli/CMakeLists.txt b/src/libCli/CMakeLists.txt new file mode 100644 index 0000000..1c9461b --- /dev/null +++ b/src/libCli/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright 2019 IBM Corporation +# +# 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. + +cmake_minimum_required (VERSION 2.8) + +set(TARGET_NAME "cli") +set(TARGET_SRC + ./MessageParsing.cpp + ./OutputFormatting.cpp + ./GrammarConstruction.cpp + ./Completion.cpp + ./Call.cpp + ) +add_library(${TARGET_NAME} ${TARGET_SRC}) +target_link_libraries ( ${TARGET_NAME} + reflection + ArgParse + ) + +if(BUILD_CONFIG_USE_BOOST_REGEX) + target_link_libraries (${TARGET_NAME} + boost_regex + ) +endif() diff --git a/src/libCli/Call.cpp b/src/libCli/Call.cpp new file mode 100644 index 0000000..c266802 --- /dev/null +++ b/src/libCli/Call.cpp @@ -0,0 +1,301 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// for detecting if we are writing stdout to terminal or to pipe/file +#include +#include + +using namespace ArgParse; + +namespace cli +{ + +// TODO: move this to OutputFormatting code +std::string customMessageFormat(const grpc::protobuf::Message & f_message, const grpc::protobuf::Descriptor* f_messageDescriptor, ParsedElement & f_customFormatParseTree, size_t startChild = 0) +{ + std::string result; + const google::protobuf::Reflection * reflection = f_message.GetReflection(); + + // first look for target fields to format: + bool found = false; + ParsedElement targetList = f_customFormatParseTree.findFirstSubTree("TargetSpecifier", found); + + if((targetList.getChildren().size() > startChild) && (targetList.getChildren()[startChild] != nullptr)) + { + std::string partialTarget = targetList.getChildren()[startChild]->findFirstChild("PartialTarget"); + //std::cout << "looking at '" << partialTarget << "'\n"; + if(partialTarget != "") + { + // empty target addresses the current message + const google::protobuf::FieldDescriptor * partialField = f_messageDescriptor->FindFieldByName(partialTarget); + if(partialField == nullptr) + { + return "No such field: " + partialTarget; + } + + // now we have three possibilities: + // 1. repeated field + // -> iterate over all instances + call recursive + return + // 2. message type field + // -> call recursive + return + // 3. normal field (terminal) + // -> continue looping + + if(partialField->is_repeated()) + { + //std::cout << "have repeated\n"; + switch(partialField->type()) + { + case grpc::protobuf::FieldDescriptor::Type::TYPE_MESSAGE: + { + + } + break; + default: + return "repeated-" + std::string(partialField->type_name()) + " is not yet supported :(\n"; + break; + } + int numberOfRepetitions = reflection->FieldSize(f_message, partialField); + for(int j = 0; j < numberOfRepetitions; j++) + { + //std::cout << " have repeated entry\n"; + const google::protobuf::Message & subMessage = reflection->GetRepeatedMessage(f_message, partialField, j); + result += customMessageFormat(subMessage, partialField->message_type(), f_customFormatParseTree, startChild+1); + } + return result; + } + if(partialField->type() == grpc::protobuf::FieldDescriptor::Type::TYPE_MESSAGE) + { + //std::cout << "have message\n"; + const google::protobuf::Message & subMessage = reflection->GetMessage(f_message, partialField); + return customMessageFormat(subMessage, partialField->message_type(), f_customFormatParseTree, startChild+1); + } + } + } + + // now we know we are not repeated and now f_message contains the correct + // context in which to evaluate field references :) + + //std::cout << "have field\n"; + OutputFormatter myOutputFormatter; + myOutputFormatter.clearColorMap(); + + bool haveFormatString = false; + auto formatString = f_customFormatParseTree.findFirstSubTree("OutputFormatString", haveFormatString); + if(not haveFormatString) + { + return "Error: no format string given\n"; + } + for(auto outputStatement : formatString.getChildren()) + { + + bool foundFieldReference = false; + auto fieldReference = outputStatement->findFirstSubTree("OutputFieldReference", foundFieldReference); + if(foundFieldReference) + { + //std::cout << " have field ref " << fieldReference.getMatchedString() << "\n"; + // need to lookup the field: + const google::protobuf::FieldDescriptor * fieldRef = f_messageDescriptor->FindFieldByName(fieldReference.getMatchedString()); + if(fieldRef == nullptr) + { + result += "???"; + } + else + { + result += myOutputFormatter.fieldValueToString(f_message, fieldRef, "", "", OutputFormatter::CustomStringModifier::Raw); + } + } + else + { + //std::cout << " have string " << outputStatement->getMatchedString() << "\n"; + result += outputStatement->getMatchedString(); + } + } + + return result; +} + +std::string getTimeString() +{ + // unfortunately std::chrono::system_clock::to_time_t() is not available + // with gcc4.8. So we use std::time and std::strftime instead. + std::time_t t = std::time(0) ; + char cstr[128] ; + std::strftime( cstr, sizeof(cstr), "%Y-%m-%d %X", std::localtime(&t) ) ; + return cstr ; +} + +int call(ParsedElement & parseTree) +{ + std::string serverAddress = parseTree.findFirstChild("ServerAddress"); + std::string serverPort = parseTree.findFirstChild("ServerPort"); + if(serverPort == "") + { + serverPort = "50051"; + } + serverAddress += ":" + serverPort; + + std::string serviceName = parseTree.findFirstChild("Service"); + std::string methodName = parseTree.findFirstChild("Method"); + bool argsExist; + ParsedElement & methodArgs = parseTree.findFirstSubTree("MethodArgs", argsExist); + + std::shared_ptr channel = + grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()); + + + grpc::ProtoReflectionDescriptorDatabase descDb(channel); + grpc::protobuf::DescriptorPool descPool(&descDb); + + const grpc::protobuf::ServiceDescriptor* service = descPool.FindServiceByName(serviceName); + if(service == nullptr) + { + std::cerr << "Error: Service '" << serviceName << "' not found" << std::endl; + return -1; + } + + auto method = service->FindMethodByName(methodName); + if(method == nullptr) + { + std::cerr << "Error: Method not found" << std::endl; + return -1; + } + + if(method->client_streaming()) + { + std::cerr << "Error: Client streaming RPCs not supported." << std::endl; + return -1; + } + + const grpc::protobuf::Descriptor* inputType = method->input_type(); + + // now we have to construct a protobuf from the parsed argument, which corresponds to the inputType + google::protobuf::DynamicMessageFactory dynamicFactory; + + // read data from the parse tree into the protobuf message: + std::unique_ptr message = cli::parseMessage(parseTree, dynamicFactory, inputType); + + + + if(parseTree.findFirstChild("PrintParsedMessage") != "") + { + // use built-in human readable output format + cli::OutputFormatter imessageFormatter; + std::cout << "Request message:" << std::endl << imessageFormatter.messageToString(*message, method->input_type(), "| ", "| " ) << std::endl; + } + + + if(not message) + { + std::cerr << "Error: Error parsing method arguments -> aborting the call :-(" << std::endl; + return -1; + } + + // now we serialize the message: + grpc::string serializedRequest; + bool success = message->SerializeToString(&serializedRequest); + if(not success) + { + std::cerr << "Error: Failed to serialize method arguments" << std::endl; + return -1; + } + + // now we do the actual RPC call: + std::multimap clientMetadata; + grpc::string serializedResponse; + std::multimap serverMetadataA; + std::multimap serverMetadataB; + + std::string methodStr = "/" + serviceName + "/" + methodName; + grpc::testing::CliCall call(channel, methodStr, clientMetadata); + call.Write(serializedRequest); + call.WritesDone(); + + // In a loop we read reply data from the reply stream: + // NOTE: in gRPC every RPC can be considered "streaming". Non-streaming RPCs + // merely return one reply message. + bool init = true; + for (init = true; call.Read(&serializedResponse, init ? &serverMetadataA : nullptr); init= false) + { + // convert data received from stream into a message: + std::unique_ptr replyMessage(dynamicFactory.GetPrototype(method->output_type())->New()); + replyMessage->ParseFromString(serializedResponse); + + // print date/time of message reception: + std::cout << getTimeString(); + std::cout << ": Received message:\n"; + + // print out string representation of the message: + std::string msgString; + + // decide on message formatting method to use: + bool customOutputFormatRequested = false; + ParsedElement customFormatParseTree = parseTree.findFirstSubTree("CustomOutputFormat", customOutputFormatRequested); + if(not customOutputFormatRequested) + { + // use built-in human readable output format + cli::OutputFormatter messageFormatter; + + // disable colored output if explicitly specified: + if(parseTree.findFirstChild("NoColor") != "") + { + messageFormatter.clearColorMap(); + } + + // automatically disable colored output, when outputting to something + // else than a terminal (pipes, files, etc.), except we explicitly + // request color mode: + if((not isatty(fileno(stdout))) and (parseTree.findFirstChild("Color") == "")) + { + messageFormatter.clearColorMap(); + } + + msgString = messageFormatter.messageToString(*replyMessage, method->output_type(), "| ", "| " ); + } + else + { + //std::cout << "using custom OutputFormatting\n"; + //std::cout << customFormatParseTree.getDebugString(); + // use user provided output format string + msgString = customMessageFormat(*replyMessage, method->output_type(), customFormatParseTree); + } + std::cout << msgString << std::endl; + } + + // reply stream finished -> finish the RPC: + grpc::Status status = call.Finish(&serverMetadataB); + + if(not status.ok()) + { + std::cerr << "RPC failed ;( Status code: " << std::to_string(status.error_code()) << ", error message: " << status.error_message() << std::endl; + return -1; + } + + std::cout << "RPC succeeded :D" << std::endl; + + + return 0; +} + +} diff --git a/src/libCli/Call.hpp b/src/libCli/Call.hpp new file mode 100644 index 0000000..b8aac51 --- /dev/null +++ b/src/libCli/Call.hpp @@ -0,0 +1,25 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once + +#include + +namespace cli +{ + /// Performs an RPC call based on information from the parse tree. + /// @param f_parseTree Parse tree containing all relevant information for the call (server address, request message, options, ...). + /// @returns 0 if RPC succeeded, -1 otherwise (including parse errors from parse tree and gRPC bad return code) + int call(ArgParse::ParsedElement & f_parseTree); +} diff --git a/src/libCli/Completion.cpp b/src/libCli/Completion.cpp new file mode 100644 index 0000000..185c4f3 --- /dev/null +++ b/src/libCli/Completion.cpp @@ -0,0 +1,91 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +using namespace ArgParse; + +namespace cli +{ + +void printBashCompletions( std::vector > & f_candidates, ParsedElement & f_parseTree, const std::string & f_args, bool f_debug) +{ + // completion requested :) + if(f_debug) + { + std::cerr << "Input string \"" << f_args << "\"\nCandidates:\n" << std::endl; + size_t n = f_parseTree.getMatchedString().size(); + for(auto candidate : f_candidates) + { + std::string candidateStr =candidate->getMatchedString(); + printf("pre: '%s'\n", candidateStr.c_str()); + } + } + + //size_t n = parseTree.getMatchedString().size(); + size_t n = f_args.size(); + for(auto candidate : f_candidates) + { + std::string candidateStr =candidate->getMatchedString(); + std::string suggestion; + size_t start = n; + size_t end; + if(f_debug) + { + printf("candidateStr[n=%zu] = '%c'\n", n, candidateStr[n]); + } + if( + (candidateStr[n] != ' ') + && + (candidateStr[n] != '=') + && + (candidateStr[n] != ',') + && + (candidateStr[n] != ':') + ) + { + // bash always expects completion suggestions to start from the last token. + // Now we need to find out where the last token has started + // We need to "simulate" bash tokenizer here. tokens are delimited by ' ' '=' or ':' + start = candidateStr.find_last_of(" =:,", n)+1; + if(start == std::string::npos) + { + start = 0; + } + end = candidateStr.find_first_of(" ", n)-1; + //printf("cand='%s', n=%zu, start=%zu, end = %zu\n",candidateStr.c_str(), n, start, end); + } + else + { + start = candidateStr.find_last_of(" =:", n-1)+1; + //end = candidateStr.find_first_of(' ', n+1); + end = n; + } + //printf("start=%zu, end=%zu\n", start, end); + //suggestion = candidateStr.substr(n, std::string::npos); + suggestion = candidateStr.substr(start, std::string::npos); + //suggestion = candidateStr.substr(start, end-start+1); + if(f_debug) + { + + printf("post: '%s'\n", suggestion.c_str()); + } + else + { + printf("%s\n", suggestion.c_str()); + } + } +} + +} diff --git a/src/libCli/Completion.hpp b/src/libCli/Completion.hpp new file mode 100644 index 0000000..2a738b8 --- /dev/null +++ b/src/libCli/Completion.hpp @@ -0,0 +1,35 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include +#include +#include + +namespace cli +{ + /// Function which prints bash completions to stdout for given list of parseTrees. + /// NOTE: this is not calculationg completions, it merely formats existing completion results + /// in a way, so that bash can handle them. + /// @param f_candidates vector of parse trees, each representing a completion candidate + /// @param f_parseTree the parstree which contains everything which could already be matched. + /// @param f_args the string given by the user which awaits completion + /// @param f_debug enables debug output if true + void printBashCompletions( + std::vector > & f_candidates, + ArgParse::ParsedElement & f_parseTree, + const std::string & f_args, + bool f_debug + ); +} diff --git a/src/libCli/GrammarConstruction.cpp b/src/libCli/GrammarConstruction.cpp new file mode 100644 index 0000000..d8faadd --- /dev/null +++ b/src/libCli/GrammarConstruction.cpp @@ -0,0 +1,455 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace ArgParse; + +namespace cli +{ + +class GrammarInjectorMethodArgs : public GrammarInjector +{ + public: + GrammarInjectorMethodArgs(Grammar & f_grammar, const std::string & f_elementName = "") : + GrammarInjector("MethodArgs", f_elementName), + m_grammar(f_grammar) + { + } + + virtual ~GrammarInjectorMethodArgs() + { + } + + virtual GrammarElement * getGrammar(ParsedElement * f_parseTree) override + { + // FIXME: we are already completing this without a service parsed. + // this works in most cases, as it will just fail. however this is not really a nice thing. + std::string serverAddress = f_parseTree->findFirstChild("ServerAddress"); + std::string serverPort = f_parseTree->findFirstChild("ServerPort"); + std::string serviceName = f_parseTree->findFirstChild("Service"); + std::string methodName = f_parseTree->findFirstChild("Method"); + + //std::cout << f_parseTree->getDebugString() << std::endl; + //std::cout << "Injecting grammar for " << serverAddress << ":" << serverPort << " " << serviceName << " " << methodName << std::endl; + if(serverPort == "") + { + serverPort = "50051"; + } + serverAddress += ":" + serverPort; + //std::cout << "Server addr: " << serverAddress << std::endl; + std::shared_ptr channel = + grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()); + + + grpc::ProtoReflectionDescriptorDatabase descDb(channel); + grpc::protobuf::DescriptorPool descPool(&descDb); + + const grpc::protobuf::ServiceDescriptor* service = descPool.FindServiceByName(serviceName); + if(service == nullptr) + { + //std::cerr << "Error: Service not found" << std::endl; + return m_grammar.createElement(""); + } + + auto method = service->FindMethodByName(methodName); + if(method == nullptr) + { + //std::cerr << "Error: Method not found" << std::endl; + return m_grammar.createElement(""); + } + + if(method->client_streaming()) + { + std::cerr << "Error: Client streaming RPCs not supported." << std::endl; + return m_grammar.createElement(""); + } + + auto concat = m_grammar.createElement(); + + auto separation = m_grammar.createElement(); + //auto separation = m_grammar.createElement(); + //separation->addChild(m_grammar.createElement()); + //separation->addChild(m_grammar.createElement(",")); + concat->addChild(separation); + + auto fields = getMessageGrammar(method->input_type()); + concat->addChild(fields); + + auto result = m_grammar.createElement("Fields"); + result->addChild(concat); + + return result; + }; + + private: + + void addFieldValueGrammar(GrammarElement * f_fieldGrammar, const grpc::protobuf::FieldDescriptor * f_field) + { + switch(f_field->cpp_type()) + { + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_FLOAT: + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_DOUBLE: + // TODO: make regex match closer + f_fieldGrammar->addChild(m_grammar.createElement("[\\+-\\.pP0-9a-fA-F]+", "FieldValue")); + break; + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_INT32: + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_INT64: + f_fieldGrammar->addChild(m_grammar.createElement("[\\+-]?(0x|0b)?[0-9a-fA-F]+", "FieldValue")); + break; + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_UINT32: + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_UINT64: + f_fieldGrammar->addChild(m_grammar.createElement("\\+?(0x|0b)?[0-9a-fA-F]+", "FieldValue")); + break; + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_BOOL: + { + auto boolGrammar = m_grammar.createElement("FieldValue"); + boolGrammar->addChild(m_grammar.createElement("true")); + boolGrammar->addChild(m_grammar.createElement("false")); + boolGrammar->addChild(m_grammar.createElement("1")); + boolGrammar->addChild(m_grammar.createElement("0")); + f_fieldGrammar->addChild(boolGrammar); + break; + } + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_ENUM: + { + const google::protobuf::EnumDescriptor * enumDesc = f_field->enum_type(); + auto enumGrammar = m_grammar.createElement("FieldValue"); + for(int i = 0; ivalue_count(); i++) + { + const google::protobuf::EnumValueDescriptor * enumValueDesc = enumDesc->value(i); + // FIXME: null possible? + enumGrammar->addChild(m_grammar.createElement(enumValueDesc->name())); + } + f_fieldGrammar->addChild(enumGrammar); + break; + } + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_STRING: + if(f_field->type() == grpc::protobuf::FieldDescriptor::Type::TYPE_BYTES) + { + auto bytesContainer = m_grammar.createElement("FieldValue"); + bytesContainer->addChild(m_grammar.createElement("0x")); + bytesContainer->addChild(m_grammar.createElement("[0-9a-fA-F]*", "")); + f_fieldGrammar->addChild(bytesContainer); + } + else + { + // FIXME: commented solution does not work: + // we always complete "::" as empty string matches + // Solution would be to change the parser to never + // attempt completion when a regex is currently + // parsed. This requires changes in the parser + // and could not be implemented on short notice. + //auto stringContainer = m_grammar.createElement("StringContainer"); + //stringContainer->addChild(m_grammar.createElement(":")); + //stringContainer->addChild(m_grammar.createElement("[^:]*", "FieldValue")); + //stringContainer->addChild(m_grammar.createElement(":")); + //f_fieldGrammar->addChild(stringContainer); + + // Using this as a workaround until parser gets better regex support + f_fieldGrammar->addChild(m_grammar.createElement("[^ ]*", "FieldValue")); + } + break; + case grpc::protobuf::FieldDescriptor::CppType::CPPTYPE_MESSAGE: + { + //std::cerr << "Field '" << field->name() << "' has message type: '" << field->type_name() << "'" << std::endl; + auto subMessage = m_grammar.createElement("FieldValue"); + subMessage->addChild(m_grammar.createElement(":")); + + auto childFieldsRep = m_grammar.createElement("Fields"); + auto concat = m_grammar.createElement(); + auto fieldsAlt = getMessageGrammar(f_field->message_type()); + concat->addChild(fieldsAlt); + + auto separation = m_grammar.createElement(); + //auto separation = m_grammar.createElement(); + //separation->addChild(m_grammar.createElement()); + //separation->addChild(m_grammar.createElement(",")); + concat->addChild(separation); + + childFieldsRep->addChild(concat); + subMessage->addChild(childFieldsRep); + + subMessage->addChild(m_grammar.createElement(":")); + f_fieldGrammar->addChild(subMessage); + break; + } + default: + std::cerr << "Field '" << f_field->name() << "' has unsupported type: '" << f_field->type_name() << "'" << std::endl; + break; + } + } + + GrammarElement * getMessageGrammar(const grpc::protobuf::Descriptor* f_messageDescriptor) + { + auto fields = m_grammar.createElement(); + // iterate over fields: + for(int i = 0; i< f_messageDescriptor->field_count(); i++) + { + const grpc::protobuf::FieldDescriptor * field = f_messageDescriptor->field(i); + + //std::cerr << "Iterating field " << std::to_string(i) << " of message " << f_messageDescriptor->name() << "with name: '" << field->name() <<"'"<< std::endl; + + // now we add grammar to the fields alternation: + auto fieldGrammar = m_grammar.createElement(); + fieldGrammar->addChild(m_grammar.createElement(field->name(), "FieldName")); + fieldGrammar->addChild(m_grammar.createElement("=")); + fields->addChild(fieldGrammar); + if(field->is_repeated()) + { + auto repeatedValue = m_grammar.createElement("RepeatedValue"); + addFieldValueGrammar(repeatedValue, field); + + auto repeatedGrammar = m_grammar.createElement("FieldValue"); + repeatedGrammar->addChild(m_grammar.createElement(":")); + + repeatedGrammar->addChild(repeatedValue); + + auto repeatedOptionalEntry = m_grammar.createElement(); + repeatedOptionalEntry->addChild(m_grammar.createElement(",")); + repeatedOptionalEntry->addChild(m_grammar.createElement()); + repeatedOptionalEntry->addChild(repeatedValue); + + auto repeatedOptionalValues = m_grammar.createElement(); + repeatedOptionalValues->addChild(repeatedOptionalEntry); + repeatedGrammar->addChild(repeatedOptionalValues); + repeatedGrammar->addChild(m_grammar.createElement(":")); + + + fieldGrammar->addChild(repeatedGrammar); + } + else + { + // the simple case: + addFieldValueGrammar(fieldGrammar, field); + } + } + + //std::cout << "Grammar generated:\n" << fields->toString() << std::endl; + return fields; + } + + + + Grammar & m_grammar; + +}; + +class GrammarInjectorMethods : public GrammarInjector +{ + public: + GrammarInjectorMethods(Grammar & f_grammar, const std::string & f_elementName = "") : + GrammarInjector("Method", f_elementName), + m_grammar(f_grammar) + { + } + + virtual GrammarElement * getGrammar(ParsedElement * f_parseTree) override + { + // FIXME: we are already completing this without a service parsed. + // this works in most cases, as it will just fail. however this is not really a nice thing. + std::string serverAddress = f_parseTree->findFirstChild("ServerAddress"); + std::string serverPort = f_parseTree->findFirstChild("ServerPort"); + std::string serviceName = f_parseTree->findFirstChild("Service"); + + //std::cout << f_parseTree->getDebugString() << std::endl; + //std::cout << "Injecting grammar for " << serverAddress << ":" << serverPort << " " << serviceName << std::endl; + if(serverPort == "") + { + serverPort = "50051"; + } + serverAddress += ":" + serverPort; + //std::cout << "Server addr: " << serverAddress << std::endl; + std::shared_ptr channel = + grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()); + + + grpc::ProtoReflectionDescriptorDatabase descDb(channel); + grpc::protobuf::DescriptorPool descPool(&descDb); + + const grpc::protobuf::ServiceDescriptor* service = descPool.FindServiceByName(serviceName); + + auto result = m_grammar.createElement(); + if(service != nullptr) + { + for (int i = 0; i < service->method_count(); ++i) + { + result->addChild(m_grammar.createElement(service->method(i)->name())); + } + } + return result; + }; + + private: + Grammar & m_grammar; + +}; + +class GrammarInjectorServices : public GrammarInjector +{ + public: + GrammarInjectorServices(Grammar & f_grammar, const std::string & f_elementName = "") : + GrammarInjector("Service", f_elementName), + m_grammar(f_grammar) + { + } + + virtual ~GrammarInjectorServices() + { + } + + virtual GrammarElement * getGrammar(ParsedElement * f_parseTree) override + { + std::string serverAddress = f_parseTree->findFirstChild("ServerAddress"); + std::string serverPort = f_parseTree->findFirstChild("ServerPort"); + if(serverPort == "") + { + serverPort = "50051"; + } + serverAddress += ":" + serverPort; + //std::cout << "Server addr: " << serverAddress << std::endl; + std::shared_ptr channel = + grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()); + + + grpc::ProtoReflectionDescriptorDatabase descDb(channel); + //grpc::protobuf::DescriptorPool desc_pool(&desc_db); + + std::vector serviceList; + if(not descDb.GetServices(&serviceList) ) + { + printf("error retrieving service list\n"); + //return -1; + } + + auto result = m_grammar.createElement(); + for(auto service : serviceList) + { + + result->addChild(m_grammar.createElement(service)); + } + return result; + }; + + private: + Grammar & m_grammar; + +}; + +GrammarElement * constructGrammar(Grammar & f_grammarPool) +{ + // user defined output formatting + // something like this will match: @.fru_info_list:found fru in slot /slot_id/: + GrammarElement * formatTargetSpecifier = f_grammarPool.createElement("CustomOutputFormat"); + formatTargetSpecifier->addChild(f_grammarPool.createElement("@")); + GrammarElement * formatTargetTree = f_grammarPool.createElement("TargetSpecifier"); + GrammarElement * formatTargetPart = f_grammarPool.createElement(); + formatTargetPart->addChild(f_grammarPool.createElement(".")); + formatTargetPart->addChild(f_grammarPool.createElement("[^:]*", "PartialTarget")); + formatTargetTree->addChild(formatTargetPart); + formatTargetSpecifier->addChild(formatTargetTree); + formatTargetSpecifier->addChild(f_grammarPool.createElement(":")); + GrammarElement * formatOutputSpecifier = f_grammarPool.createElement("OutputFormatString"); + GrammarElement * formatOutputSpecifierAlternation = f_grammarPool.createElement(); + formatOutputSpecifierAlternation->addChild(f_grammarPool.createElement("[^:/]+", "OutputFixedString")); // a real string + GrammarElement * fieldReference = f_grammarPool.createElement(); + fieldReference->addChild(f_grammarPool.createElement("/")); + fieldReference->addChild(f_grammarPool.createElement("[^/,]*", "OutputFieldReference")); // field reference + GrammarElement * modifiers = f_grammarPool.createElement("OutputFormatModifiers"); + GrammarElement * modifierConcat = f_grammarPool.createElement(); + modifierConcat->addChild(f_grammarPool.createElement(",")); + GrammarElement * modifierAlternation = f_grammarPool.createElement("Modifier"); + modifierAlternation->addChild(f_grammarPool.createElement("hex")); + modifierAlternation->addChild(f_grammarPool.createElement("dec")); + modifierAlternation->addChild(f_grammarPool.createElement("zeroPadding")); + modifierAlternation->addChild(f_grammarPool.createElement("spacePadding")); + modifierAlternation->addChild(f_grammarPool.createElement("noPadding")); + modifiers->addChild(modifierConcat); + modifierConcat->addChild(modifierAlternation); + fieldReference->addChild(modifiers); + fieldReference->addChild(f_grammarPool.createElement("/")); + formatOutputSpecifierAlternation->addChild(fieldReference); + formatOutputSpecifier->addChild(formatOutputSpecifierAlternation); + + formatTargetSpecifier->addChild(formatOutputSpecifier); + formatTargetSpecifier->addChild(f_grammarPool.createElement(":")); + + GrammarElement * customOutputFormat = f_grammarPool.createElement(); + customOutputFormat->addChild(f_grammarPool.createElement("--customOutput")); + customOutputFormat->addChild(f_grammarPool.createElement()); + customOutputFormat->addChild(formatTargetSpecifier); + // TODO add this to options + + // options + GrammarElement * options = f_grammarPool.createElement(); // TODO: support multiple options + GrammarElement * optionsconcat = f_grammarPool.createElement(); + GrammarElement * optionsalt = f_grammarPool.createElement(); + optionsalt->addChild(f_grammarPool.createElement("-h", "Help")); + optionsalt->addChild(f_grammarPool.createElement("--help", "Help")); + optionsalt->addChild(f_grammarPool.createElement("--complete", "Complete")); + optionsalt->addChild(f_grammarPool.createElement("--debugComplete", "CompleteDebug")); + optionsalt->addChild(f_grammarPool.createElement("--dot", "DotExport")); + optionsalt->addChild(f_grammarPool.createElement("--noColor", "NoColor")); + optionsalt->addChild(f_grammarPool.createElement("--color", "Color")); + optionsalt->addChild(f_grammarPool.createElement("--version", "Version")); + optionsalt->addChild(f_grammarPool.createElement("--printParsedMessage", "PrintParsedMessage")); + optionsalt->addChild(customOutputFormat); + // FIXME FIXME FIXME: we cannot distinguish between --complete and --completeDebug.. this is a problem for arguments too, as we cannot guarantee, that we do not have an argument starting with the name of an other argument. + // -> could solve by makeing FixedString greedy + optionsconcat->addChild(optionsalt); + optionsconcat->addChild( + f_grammarPool.createElement() + ); + //optionsconcat->addChild( + // f_grammarPool.createElement()->addChild( + // f_grammarPool.createElement() + // ) + // ); + options->addChild(optionsconcat); + + // Server address + GrammarElement * serverAddress = f_grammarPool.createElement("ServerAddress"); + serverAddress->addChild(f_grammarPool.createElement("\\d+\\.\\d+\\.\\d+\\.\\d+", "IPv4Address")); + serverAddress->addChild(f_grammarPool.createElement("\\[?[0-9a-fA-F:]+\\]?", "IPv6Address")); + serverAddress->addChild(f_grammarPool.createElement("[^\\.:\\[\\] ]+", "Hostname")); + + // Server port + GrammarElement * cServerPort = f_grammarPool.createElement(); + cServerPort->addChild(f_grammarPool.createElement(":")); + cServerPort->addChild(f_grammarPool.createElement("\\d+", "ServerPort")); + GrammarElement * serverPort = f_grammarPool.createElement(); + serverPort->addChild(cServerPort); + + //GrammarElement * testAlt = f_grammarPool.createElement("TestAlt"); + //testAlt->addChild(f_grammarPool.createElement("challo")); + //testAlt->addChild(f_grammarPool.createElement("ctschuess")); + + // main concat: + GrammarElement * cmain = f_grammarPool.createElement(); + cmain->addChild(options); + cmain->addChild(serverAddress); + cmain->addChild(serverPort); + //cmain->addChild(testAlt); + //cmain->addChild(f_grammarPool.createElement(std::regex("\\S+"), "ServerAddress")); + cmain->addChild(f_grammarPool.createElement()); + cmain->addChild(f_grammarPool.createElement(f_grammarPool, "Service")); + cmain->addChild(f_grammarPool.createElement()); + cmain->addChild(f_grammarPool.createElement(f_grammarPool, "Method")); + //cmain->addChild(f_grammarPool.createElement()); + cmain->addChild(f_grammarPool.createElement(f_grammarPool, "MethodArgs")); + + return cmain; +} +} diff --git a/src/libCli/GrammarConstruction.hpp b/src/libCli/GrammarConstruction.hpp new file mode 100644 index 0000000..664eb56 --- /dev/null +++ b/src/libCli/GrammarConstruction.hpp @@ -0,0 +1,26 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once + +#include + +namespace cli +{ + /// Constructs the grammar for the gWhisper CLI. + /// @param f_grammarPool Pool to allocate grammar elements from. + /// @returns the root element of the generated grammar. The pointer should not + /// be used after the given f_grammarPool is de-allocated. + ArgParse::GrammarElement * constructGrammar(ArgParse::Grammar & f_grammarPool); +} diff --git a/src/libCli/MessageParsing.cpp b/src/libCli/MessageParsing.cpp new file mode 100644 index 0000000..391b064 --- /dev/null +++ b/src/libCli/MessageParsing.cpp @@ -0,0 +1,296 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +using namespace ArgParse; + +namespace cli +{ + +/// Parses a single field falue from a given parse tree into a protobuf message. +/// @param f_parseTree Parse tree containing the field value information. +/// @param f_message protobuf message to which the field value should be added +/// @param f_factory Factory for creation of additional messages (required for nested messages) +/// @param f_fieldDescriptor Descriptor describing the type of the field +/// @param f_isRepeated if true, field value will be added as a repeated field value. +/// (protobuf reflection api unfortunately does not provide a combined API for setting unique fields and adding to repeated fields) +/// @returns 0 if field value could be added to the message. -1 otherwise. +int parseFieldValue(ParsedElement & f_parseTree, google::protobuf::Message * f_message, google::protobuf::DynamicMessageFactory & f_factory, const google::protobuf::FieldDescriptor * f_fieldDescriptor, bool f_isRepeated = false) +{ + const google::protobuf::Reflection* reflection = f_message->GetReflection(); + std::string valueString = f_parseTree.findFirstChild("FieldValue"); + + switch(f_fieldDescriptor->cpp_type()) + { + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_FLOAT: + { + float value = std::stod(valueString); + if(f_isRepeated) + { + reflection->AddFloat(f_message, f_fieldDescriptor, value); + } + else + { + reflection->SetFloat(f_message, f_fieldDescriptor, value); + } + } + break; + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_DOUBLE: + { + double value = std::stod(valueString); + if(f_isRepeated) + { + reflection->AddDouble(f_message, f_fieldDescriptor, value); + } + else + { + reflection->SetDouble(f_message, f_fieldDescriptor, value); + } + } + break; + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_INT32: + { + long value = std::stol(valueString, 0, 0); + if(f_isRepeated) + { + reflection->AddInt32(f_message, f_fieldDescriptor, value); + } + else + { + reflection->SetInt32(f_message, f_fieldDescriptor, value); + } + } + break; + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_INT64: + { + long value = std::stol(valueString, 0, 0); + if(f_isRepeated) + { + reflection->AddInt64(f_message, f_fieldDescriptor, value); + } + else + { + reflection->SetInt64(f_message, f_fieldDescriptor, value); + } + } + break; + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_UINT32: + { + unsigned long value = std::stoul(valueString, 0, 0); + if(f_isRepeated) + { + reflection->AddUInt32(f_message, f_fieldDescriptor, value); + } + else + { + reflection->SetUInt32(f_message, f_fieldDescriptor, value); + } + } + break; + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_UINT64: + { + unsigned long value = std::stoul(valueString, 0, 0); + if(f_isRepeated) + { + reflection->AddUInt64(f_message, f_fieldDescriptor, value); + } + else + { + reflection->SetUInt64(f_message, f_fieldDescriptor, value); + } + } + break; + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_BOOL: + if( valueString == "1" || valueString == "true" || valueString == "True" ) + { + if(f_isRepeated) + { + reflection->AddBool(f_message, f_fieldDescriptor, true); + } + else + { + reflection->SetBool(f_message, f_fieldDescriptor, true); + } + } + else + { + if(f_isRepeated) + { + reflection->AddBool(f_message, f_fieldDescriptor, false); + } + else + { + reflection->SetBool(f_message, f_fieldDescriptor, false); + } + } + break; + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_ENUM: + { + const google::protobuf::EnumValueDescriptor * enumVal = f_fieldDescriptor->enum_type()->FindValueByName(valueString); + if(enumVal != nullptr) + { + if(f_isRepeated) + { + reflection->AddEnum(f_message, f_fieldDescriptor, enumVal); + } + else + { + reflection->SetEnum(f_message, f_fieldDescriptor, enumVal); + } + } + else + { + std::cerr << "Error parsing enum for field '" << f_fieldDescriptor->name() << "'" << std::endl; + return -1; + } + } + break; + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_STRING: + // we could have a string or a bytes input here + if(f_fieldDescriptor->type() == google::protobuf::FieldDescriptor::Type::TYPE_BYTES) + { + std::string resultString; + // if we have a bytes field, we parse a hex string: + if(valueString.substr(0,2) != "0x") + { + std::cerr << "Error parsing bytes field '" << f_fieldDescriptor->name() << "': Given value does not start with '0x'. Expected a hex string" << std::endl; + return -1; + } + if(valueString.size()%2 != 0) + { + std::cerr << "Error parsing bytes field '" << f_fieldDescriptor->name() << "': Given value is not a multiple of 8 bits long" << std::endl; + return -1; + } + for(size_t pos = 2; pos+1AddString(f_message, f_fieldDescriptor, resultString); + } + else + { + reflection->SetString(f_message, f_fieldDescriptor, resultString); + } + } + else + { + // otherwise we directly parse the given string: + if(f_isRepeated) + { + reflection->AddString(f_message, f_fieldDescriptor, valueString); + } + else + { + reflection->SetString(f_message, f_fieldDescriptor, valueString); + } + } + break; + case google::protobuf::FieldDescriptor::CppType::CPPTYPE_MESSAGE: + { + const google::protobuf::Descriptor * subMessageDescriptor = f_fieldDescriptor->message_type(); + std::unique_ptr subMessage = parseMessage(f_parseTree, f_factory, subMessageDescriptor); + if(subMessage != nullptr) + { + if(f_isRepeated) + { + reflection->AddAllocatedMessage(f_message, f_fieldDescriptor, subMessage.release()); + } + else + { + reflection->SetAllocatedMessage(f_message, subMessage.release(), f_fieldDescriptor); + } + } + else + { + std::cerr << "Error parsing sub-message for field '" << f_fieldDescriptor->name() << "'" << std::endl; + return -1; + } + } + break; + default: + std::cerr << "Error: Parsing Field '" << f_fieldDescriptor->name() << "'. It has the unsupported type: '" << f_fieldDescriptor->type_name() << "'" << std::endl; + return -1; + + } + + return 0; +} + +std::unique_ptr parseMessage(ParsedElement & f_parseTree, google::protobuf::DynamicMessageFactory & f_factory, const google::protobuf::Descriptor* f_messageDescriptor) +{ + std::unique_ptr message(f_factory.GetPrototype(f_messageDescriptor)->New()); + + // we iterate over all fields: + bool found = false; + ParsedElement & parsedFields = f_parseTree.findFirstSubTree("Fields", found); + if(not found) + { + std::cerr << "Error: no Fields found in parseTree for message '" << f_messageDescriptor->name() << "'" << std::endl; + return nullptr; + } + //std::cout << "Parsing message from tree: \n" << f_parseTree.getDebugString(" ") << std::endl; + int rc = 0; + for(std::shared_ptr parsedField : parsedFields.getChildren()) + { + if(parsedField->isCompletelyParsed()) + { + //std::cout << "Parsing field from tree: \n" << parsedField->getDebugString(" ") << std::endl; + const google::protobuf::FieldDescriptor * fieldDescriptor = f_messageDescriptor->FindFieldByName(parsedField->findFirstChild("FieldName")); + if(fieldDescriptor == nullptr) + { + std::cerr << "Warning: Field '" << parsedField->findFirstChild("FieldName") << "' does not exist. Ignoring." << std::endl; + continue; + } + + // now we have to parse the field value according to its type: + ParsedElement & fieldValue = parsedField->findFirstSubTree("FieldValue", found); + if(found) + { + if(fieldDescriptor->is_repeated()) + { + std::vector repeatedFieldValues; + // note: the f_doNotSearchChildsOfMatchingElements flag needs to be set to true here + // this ensures, that we can have repeated fields as part of repeated messages + fieldValue.findAllSubTrees("RepeatedValue", repeatedFieldValues, true); + for(auto repeatedValue : repeatedFieldValues) + { + rc = parseFieldValue(*repeatedValue, message.get(), f_factory, fieldDescriptor, true); + } + } + else + { + rc = parseFieldValue(fieldValue, message.get(), f_factory, fieldDescriptor); + } + } + else + { + std::cerr << "Error: No Value given for field '" << parsedField->findFirstChild("FieldName") << "'" << std::endl; + return nullptr; + } + if(rc != 0) + { + message.reset(); + break; + } + } + } + + return std::move(message); +} + +} diff --git a/src/libCli/MessageParsing.hpp b/src/libCli/MessageParsing.hpp new file mode 100644 index 0000000..5b8b733 --- /dev/null +++ b/src/libCli/MessageParsing.hpp @@ -0,0 +1,35 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once + +#include +#include + +namespace cli +{ + /// Constructs a gRPC message from a given parseTree. + /// From a given parse tree, this function constructs a protobuf message and fills + /// it with all data available from the parse tree. + /// @param f_parseTree The parse tree containing al information which should be "parsed" into the protobuf message. + /// @param f_factory Required to construct messages. + /// @param f_messageDescriptor Message descriptor describing the type of the messache whoch should be constructed. + /// @returns unique_ptr pointing to a newly created message if parse succedded, + /// or an unassociated unique_ptr if parse failed. + std::unique_ptr parseMessage( + ArgParse::ParsedElement & f_parseTree, + google::protobuf::DynamicMessageFactory & f_factory, + const google::protobuf::Descriptor* f_messageDescriptor + ); +} diff --git a/src/libCli/OutputFormatting.cpp b/src/libCli/OutputFormatting.cpp new file mode 100644 index 0000000..8182b01 --- /dev/null +++ b/src/libCli/OutputFormatting.cpp @@ -0,0 +1,460 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace cli +{ + + template + std::string OutputFormatter::intToHexString(T f_value) + { + std::stringstream sstream; + + sstream << getColor(ColorClass::HexValue) + << "0x" + << std::hex << std::setfill ('0') + << std::setw(2 * sizeof(T)) + << f_value + << getColor(ColorClass::Normal); + return sstream.str(); + } + + OutputFormatter::OutputFormatter() : + m_colorMap{ + {ColorClass::Normal, "\e[0m\e[39m"}, + {ColorClass::VerticalGuides, "\e[2m\e[37m"}, + {ColorClass::HorizontalGuides, "\e[2m\e[37m"}, + {ColorClass::NonRepeatedFieldName, "\e[94m"}, + {ColorClass::RepeatedFieldName, "\e[34m"}, + {ColorClass::RepeatedCount, "\e[33m"}, + {ColorClass::BoolTrue, "\e[32m"}, + {ColorClass::BoolFalse, "\e[31m"}, + {ColorClass::StringValue, "\e[33m"}, + {ColorClass::MessageTypeName, "\e[35m"}, + {ColorClass::DecimalValue, "\e[39m"}, + {ColorClass::HexValue, "\e[39m"}, + {ColorClass::EnumValue, "\e[33m"}, + } + { + + } + + void OutputFormatter::clearColorMap() + { + m_colorMap.clear(); + } + + std::string OutputFormatter::getColor(OutputFormatter::ColorClass f_colorClass) + { + auto resultIt = m_colorMap.find(f_colorClass); + if(resultIt != m_colorMap.end()) + { + return resultIt->second; + } + else + { + return ""; + } + } + + std::string OutputFormatter::colorize(OutputFormatter::ColorClass f_colorClass, const std::string & f_string) + { + return getColor(f_colorClass) + f_string + getColor(ColorClass::Normal); + } + + std::string OutputFormatter::generateHorizontalGuide(size_t f_currentSize, size_t f_targetSize) + { + std::string result = getColor(ColorClass::HorizontalGuides); + for(;f_currentSizetype()) + { + case grpc::protobuf::FieldDescriptor::Type::TYPE_MESSAGE: + { + const google::protobuf::Message & subMessage = reflection->GetRepeatedMessage(f_message, f_fieldDescriptor, f_fieldIndex); + //result += "\n" + f_currentPrefix + f_initPrefix + ":\n"; + result += colorize(ColorClass::MessageTypeName, std::string("{") + f_fieldDescriptor->message_type()->name() + "}"); + result += "\n"; + result += messageToString(subMessage,f_fieldDescriptor->message_type(), f_initPrefix, f_currentPrefix+f_initPrefix); + //result += "\n" + f_currentPrefix + f_initPrefix + ":"; + + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_SFIXED32: + case grpc::protobuf::FieldDescriptor::Type::TYPE_SINT32: + case grpc::protobuf::FieldDescriptor::Type::TYPE_INT32: + { + int32_t value = reflection->GetRepeatedInt32(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromInt(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_SFIXED64: + case grpc::protobuf::FieldDescriptor::Type::TYPE_SINT64: + case grpc::protobuf::FieldDescriptor::Type::TYPE_INT64: + { + int64_t value = reflection->GetRepeatedInt64(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromInt(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_FIXED32: + case grpc::protobuf::FieldDescriptor::Type::TYPE_UINT32: + { + uint32_t value = reflection->GetRepeatedUInt32(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromUInt(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_FIXED64: + case grpc::protobuf::FieldDescriptor::Type::TYPE_UINT64: + { + uint64_t value = reflection->GetRepeatedUInt64(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromUInt(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_FLOAT: + { + float value = reflection->GetRepeatedFloat(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromFloat(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_DOUBLE: + { + double value = reflection->GetRepeatedDouble(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromFloat(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_BOOL: + { + bool value = reflection->GetRepeatedBool(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromBool(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_STRING: + { + std::string value = reflection->GetRepeatedString(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromString(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_ENUM: + { + const google::protobuf::EnumValueDescriptor * value = reflection->GetRepeatedEnum(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromEnum(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_BYTES: + { + std::string value = reflection->GetRepeatedString(f_message, f_fieldDescriptor, f_fieldIndex); + result += stringFromBytes(value, f_modifier, f_currentPrefix + f_initPrefix); + } + break; + default: + return "repeated-" + std::string(f_fieldDescriptor->type_name()) + " is not yet supported :("; + break; + } + + return result; +} + +std::string OutputFormatter::fieldValueToString(const grpc::protobuf::Message & f_message, const google::protobuf::FieldDescriptor * f_fieldDescriptor, const std::string & f_initPrefix, const std::string & f_currentPrefix, CustomStringModifier f_modifier) +{ + const google::protobuf::Reflection * reflection = f_message.GetReflection(); + std::string result; + + // first, we need to check if this field is part of a OneOf: + const google::protobuf::OneofDescriptor * oneOfDesc = f_fieldDescriptor->containing_oneof(); + if(oneOfDesc != nullptr) + { + // yes we are part of a OneOf... + // so, we check if we are the field which is set as the OneOfType + if(reflection->GetOneofFieldDescriptor(f_message, oneOfDesc) != f_fieldDescriptor) + { + // no we are not set -> Do not continue to stringify this field, + // as it is not set. Instead we add [NOT SET] to the field string: + result += "[NOT SET]"; + // no need to decode any further... + return result; + } + } + + switch(f_fieldDescriptor->type()) + { + case grpc::protobuf::FieldDescriptor::Type::TYPE_SFIXED32: + case grpc::protobuf::FieldDescriptor::Type::TYPE_SINT32: + case grpc::protobuf::FieldDescriptor::Type::TYPE_INT32: + { + int32_t value = reflection->GetInt32(f_message, f_fieldDescriptor); + result += stringFromInt(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_SFIXED64: + case grpc::protobuf::FieldDescriptor::Type::TYPE_SINT64: + case grpc::protobuf::FieldDescriptor::Type::TYPE_INT64: + { + int64_t value = reflection->GetInt64(f_message, f_fieldDescriptor); + result += stringFromInt(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_FIXED32: + case grpc::protobuf::FieldDescriptor::Type::TYPE_UINT32: + { + uint32_t value = reflection->GetUInt32(f_message, f_fieldDescriptor); + result += stringFromUInt(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_FIXED64: + case grpc::protobuf::FieldDescriptor::Type::TYPE_UINT64: + { + uint64_t value = reflection->GetUInt64(f_message, f_fieldDescriptor); + result += stringFromUInt(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_FLOAT: + { + float value = reflection->GetFloat(f_message, f_fieldDescriptor); + result += stringFromFloat(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_DOUBLE: + { + double value = reflection->GetDouble(f_message, f_fieldDescriptor); + result += stringFromFloat(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_BOOL: + { + bool value = reflection->GetBool(f_message, f_fieldDescriptor); + result += stringFromBool(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_STRING: + { + std::string value = reflection->GetString(f_message, f_fieldDescriptor); + result += stringFromString(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_ENUM: + { + const google::protobuf::EnumValueDescriptor * value = reflection->GetEnum(f_message, f_fieldDescriptor); + result += stringFromEnum(value, f_modifier); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_BYTES: + { + std::string value = reflection->GetString(f_message, f_fieldDescriptor); + result += stringFromBytes(value, f_modifier, f_currentPrefix + f_initPrefix); + } + break; + case grpc::protobuf::FieldDescriptor::Type::TYPE_MESSAGE: + { + const google::protobuf::Message & subMessage = reflection->GetMessage(f_message, f_fieldDescriptor); + //result += ":\n"; + result += colorize(ColorClass::MessageTypeName, std::string("{") + f_fieldDescriptor->message_type()->name() + "}"); + result += "\n"; + result += messageToString(subMessage,f_fieldDescriptor->message_type(), f_initPrefix, f_currentPrefix+f_initPrefix); + //result += "\n" + f_currentPrefix + f_initPrefix + ":"; + } + break; + default: + return std::string(f_fieldDescriptor->type_name()) + " is not yet supported :("; + break; + } + + return result; +} + +std::string OutputFormatter::fieldToString(const grpc::protobuf::Message & f_message, const google::protobuf::FieldDescriptor * f_fieldDescriptor, const std::string & f_initPrefix, const std::string & f_currentPrefix, size_t maxFieldNameSize) +{ + std::string result; + const google::protobuf::Reflection * reflection = f_message.GetReflection(); + + if(f_fieldDescriptor->is_repeated()) + { + int numberOfRepetitions = reflection->FieldSize(f_message, f_fieldDescriptor); + if(numberOfRepetitions == 0) + { + // TODO: remove duplicate code + result += colorize(ColorClass::VerticalGuides, f_currentPrefix); + std::string repName; + repName += colorize(ColorClass::RepeatedFieldName, f_fieldDescriptor->name()); + repName += colorize(ColorClass::RepeatedCount, "[0/0]"); + result += repName; + size_t nameSize = repName.size(); + result += generateHorizontalGuide(nameSize, maxFieldNameSize); + result += " = " + colorize(ColorClass::MessageTypeName, "{}"); + } + for(int i = 0; i < numberOfRepetitions; i++) + { + if(i!=0) + { + result += "\n"; + } + result += colorize(ColorClass::VerticalGuides, f_currentPrefix); + std::string repName; + repName += getColor(ColorClass::RepeatedFieldName) + f_fieldDescriptor->name() + getColor(ColorClass::Normal); + repName += getColor(ColorClass::RepeatedCount) + "[" + std::to_string(i+1) + "/" + std::to_string(numberOfRepetitions) + "]" + getColor(ColorClass::Normal); + result += repName; + size_t nameSize = repName.size(); + result += generateHorizontalGuide(nameSize, maxFieldNameSize); + result += " = "; + result += repeatedFieldValueToString(f_message, f_fieldDescriptor, f_initPrefix, f_currentPrefix, i); + } + + } + else + { + result += colorize(ColorClass::VerticalGuides, f_currentPrefix); + result += colorize(ColorClass::NonRepeatedFieldName, f_fieldDescriptor->name()); + size_t nameSize = f_fieldDescriptor->name().size(); + result += generateHorizontalGuide(nameSize, maxFieldNameSize); + result += " = "; + result += fieldValueToString(f_message, f_fieldDescriptor, f_initPrefix, f_currentPrefix); + } + + return result; +} + +std::string OutputFormatter::messageToString(const grpc::protobuf::Message & f_message, const grpc::protobuf::Descriptor* f_messageDescriptor, const std::string & f_initPrefix, const std::string & f_currentPrefix) +{ + std::string result; + // first determine field name length maximum (for aligned formatting) + size_t maxFieldNameLength = 0; + for(int i = 0; i< f_messageDescriptor->field_count(); i++) + { + const google::protobuf::FieldDescriptor * fieldDesc = f_messageDescriptor->field(i); + size_t thisFieldNameLength = fieldDesc->name().size(); + + if(fieldDesc->is_repeated()) + { + const google::protobuf::Reflection * reflection = f_message.GetReflection(); + int numberOfRepetitions = reflection->FieldSize(f_message, fieldDesc); + std::string simulatedMaxArr = "[" + std::to_string(numberOfRepetitions) + "/" + std::to_string(numberOfRepetitions) + "]"; + thisFieldNameLength += simulatedMaxArr.size(); + + + } + if(thisFieldNameLength > maxFieldNameLength) + { + maxFieldNameLength = thisFieldNameLength; + } + } + + for(int i = 0; i< f_messageDescriptor->field_count(); i++) + { + const google::protobuf::FieldDescriptor * fieldDesc = f_messageDescriptor->field(i); + if(i!=0) + { + result += "\n"; + } + result += fieldToString(f_message, fieldDesc, f_initPrefix, f_currentPrefix, maxFieldNameLength); + } + return result; +} + +std::string OutputFormatter::stringFromBytes(const std::string & f_value, const CustomStringModifier & f_modifier, const std::string & f_prefix) +{ + std::string result; + // a simple hexdump: + const std::string & prefix = f_prefix; + result += "hex[" + std::to_string(f_value.size()) + "]"; + + std::string stringRepresentation = ""; + size_t maxAddrTextSize = std::to_string(f_value.size()-1).size(); + for(size_t i = 0; i 8) + { + result += "\n" + prefix; + std::stringstream streamAddr; + streamAddr << std::setfill (' ') << std::setw(maxAddrTextSize); + // TODO: should place address as hex also... + streamAddr << i; + result += streamAddr.str(); + result += ": "; + } + else + { + result += " = "; + } + } + else if(i%4 == 0) + { + result += " "; + } + else + { + result += " "; + } + } + + // now do the actual hexdump: + // cast to uint16_t necessary, otherwise stream interprets the value as a char. + uint16_t byteVal = f_value[i] & 0xff; + std::stringstream stream; + stream << std::setfill ('0') << std::setw(2) << std::hex; + stream << byteVal; + result += stream.str(); + + // create string representation: + if( (f_value[i] >= 32) and (f_value[i] <= 126) ) + { + // string representable character range: + stringRepresentation += f_value[i]; + } + else + { + // special characters + stringRepresentation += "."; + } + + i++; + + // Place string representation, when appropriate: + if( (i%8 == 0) or (i>=f_value.size()) ) + { + size_t padding = (8 - i%8)%8; + if(padding>3) + { + padding *=3; + padding += 1; + } + else + { + padding *=3; + } + result += std::string(padding, ' '); + result += " |" + stringRepresentation + "|"; + stringRepresentation = ""; + } + } + return result; +} +} diff --git a/src/libCli/OutputFormatting.hpp b/src/libCli/OutputFormatting.hpp new file mode 100644 index 0000000..50d08e7 --- /dev/null +++ b/src/libCli/OutputFormatting.hpp @@ -0,0 +1,150 @@ +// Copyright 2019 IBM Corporation +// +// 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. + +#pragma once +#include + +#include +#include + +namespace cli +{ + /// Class with methods to format a protobuf message into human readable strings. + class OutputFormatter + { + public: + /// Initializes the OutputFormatter with default settings. + /// Includes initialization of the default color theme. + OutputFormatter(); + + /// Enum containing all possible elements for colorization. + enum class ColorClass + { + Normal, // Used for all text not covered by a color class + NonRepeatedFieldName, // Field name of non-repeated fields + RepeatedFieldName, // Field name of repeated fields + VerticalGuides, // The indentation string + HorizontalGuides, // The filling string (dots) from fieldname to value + RepeatedCount, // Counter (both: current and total) of repeated fields + BoolTrue, // true value of bools + BoolFalse, // false value of bools + StringValue, // string values + MessageTypeName, // type name of messages + DecimalValue, // devimal values of numbers + HexValue, // hex value of numbers + EnumValue // enum values + }; + + /// Possible modifiers, which may be used to control how the formatter converts certain types into string. + enum class CustomStringModifier + { + None, // does not change behavior + Raw // Prints integers in hex and without colorization. TODO: better name + }; + + /// Formats a protobuf message into a human readable string. + /// @param f_message the protobuf message to be formatted + /// @param f_messageDescriptor descriptor describing the message type + /// @param f_initPrefix Each new line in formatted output will be prefixed with 0 or more instances of this string corresponding to the indentation level. + /// @param f_currentPrefix Each new line in formatted output will be prefixed with this sting. May be used for an initial prefix/indentation of output text. + std::string messageToString( + const grpc::protobuf::Message & f_message, + const grpc::protobuf::Descriptor* f_messageDescriptor, + const std::string & f_initPrefix = " ", + const std::string & f_currentPrefix = "" + ); + + /// Clears the color map. + /// Causes all output to be generated with default font (no terminal control characters). + void clearColorMap(); + + // TODO: provide the option to provide custom color map + // e.g. via config file or cli args + + /// Formats a field value as string. + /// NOTE: required for custom output format + std::string fieldValueToString(const grpc::protobuf::Message & f_message, const google::protobuf::FieldDescriptor * f_fieldDescriptor, const std::string & f_initPrefix, const std::string & f_currentPrefix, CustomStringModifier f_modifier = CustomStringModifier::None); + + /// Formats a repeated field value as string. + /// NOTE: required for custom output format + std::string repeatedFieldValueToString(const grpc::protobuf::Message & f_message, const google::protobuf::FieldDescriptor * f_fieldDescriptor, const std::string & f_initPrefix, const std::string & f_currentPrefix, int f_fieldIndex, CustomStringModifier f_modifier = CustomStringModifier::None); + + private: + std::map m_colorMap; + std::string generateHorizontalGuide(size_t f_currentSize, size_t f_targetSize); + std::string getColor(ColorClass f_colorClass); + std::string colorize(ColorClass f_colorClass, const std::string & f_string); + std::string fieldToString(const grpc::protobuf::Message & f_message, const google::protobuf::FieldDescriptor * f_fieldDescriptor, const std::string & f_initPrefix, const std::string & f_currentPrefix, size_t maxFieldNameSize); + template std::string intToHexString(T f_value); + + // string formatting methods for various types: + template + std::string stringFromInt(T f_value, const CustomStringModifier & f_modifier) + { + std::string result; + if(f_modifier == CustomStringModifier::Raw) + { + result += intToHexString(f_value); + } + else + { + result += colorize(ColorClass::DecimalValue, std::to_string(f_value)); + } + return result; + } + + template + std::string stringFromUInt(T f_value, const CustomStringModifier & f_modifier) + { + std::string result; + if(f_modifier == CustomStringModifier::Raw) + { + result += intToHexString(f_value); + } + else + { + std::stringstream stream; + stream << colorize(ColorClass::DecimalValue, std::to_string(f_value)) + << " (" << intToHexString(f_value) << ")" + << getColor(ColorClass::Normal); + result += stream.str(); + } + return result; + } + + template + std::string stringFromFloat(T f_value, const CustomStringModifier & f_modifier) + { + return colorize(ColorClass::DecimalValue, std::to_string(f_value)); + } + + std::string stringFromBool(bool f_value, const CustomStringModifier & f_modifier) + { + return (f_value ? colorize(ColorClass::BoolTrue,"true") : colorize(ColorClass::BoolFalse,"false")); + } + + std::string stringFromString(const std::string & f_value, const CustomStringModifier & f_modifier) + { + return colorize(ColorClass::StringValue, "\"" + f_value + "\""); + } + + std::string stringFromEnum(const google::protobuf::EnumValueDescriptor * f_value, const CustomStringModifier & f_modifier) + { + return colorize(ColorClass::EnumValue, f_value->name()); + } + + std::string stringFromBytes(const std::string & f_value, const CustomStringModifier & f_modifier, const std::string & f_prefix); + + }; +} diff --git a/tests/AlternationTest.cpp b/tests/AlternationTest.cpp new file mode 100644 index 0000000..f082384 --- /dev/null +++ b/tests/AlternationTest.cpp @@ -0,0 +1,312 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +using namespace ArgParse; + +// ----------------------------------------------------------------------------- +// Alternation +// ----------------------------------------------------------------------------- + +TEST(AlternationTest, NoChildEmptyString) { + EXPECT_EQ(true, true); + + Alternation myAlternation; + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myAlternation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); +} + +TEST(AlternationTest, NoChildNonEmptyString) { + Alternation myAlternation; + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myAlternation.parse("test", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); +} + +TEST(AlternationTest, OneChildEmptyString) { + FixedString child1("child1"); + Alternation myAlternation; + myAlternation.addChild(&child1); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myAlternation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myAlternation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); +} + +TEST(AlternationTest, TwoChildsSimilarStart) { + FixedString child1("child1"); + FixedString child2("child12345"); + Alternation myAlternation; + myAlternation.addChild(&child1); + myAlternation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myAlternation.parse("child12345", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(strlen("child12345"), rc.lenParsed); + EXPECT_EQ(strlen("child12345"), rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); +} + +TEST(AlternationTest, TwoChildEmptyString) { + FixedString child1("child1"); + FixedString child2("child2"); + Alternation myAlternation; + myAlternation.addChild(&child1); + myAlternation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myAlternation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("child1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myAlternation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("child2", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&myAlternation, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); +} + +TEST(AlternationTest, TwoChildCorrectStartString) { + FixedString child1("child1"); + FixedString child2("child2"); + Alternation myAlternation; + myAlternation.addChild(&child1); + myAlternation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myAlternation.parse("child", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(strlen("child"), rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("child1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myAlternation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("child2", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&myAlternation, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); +} + +TEST(AlternationTest, TwoChildUniquePartialMatch) { + FixedString child1("child1asd"); + FixedString child2("child2fgh"); + Alternation myAlternation; + myAlternation.addChild(&child1); + myAlternation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myAlternation.parse("child2", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(strlen("child2"), rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child2fgh", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myAlternation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); + EXPECT_EQ(&child2, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); +} + +TEST(AlternationTest, TwoChildWrongString) { + FixedString child1("child1"); + FixedString child2("child2"); + Alternation myAlternation; + myAlternation.addChild(&child1); + myAlternation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myAlternation.parse("asd", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::unexpectedText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); +} + +TEST(AlternationTest, TwoChildCorrectStringForBoth) { + FixedString child1("child1"); + FixedString child2("child123"); + Alternation myAlternation; + myAlternation.addChild(&child1); + myAlternation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myAlternation.parse("child1", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(6, rc.lenParsed); + EXPECT_EQ(6, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + + // parsedElement + // hmmm what to expect here: one or two? i.e. is the matched one also a candidate (maybe relevant for further completion...) + // -> hm maybe not: maybe we should in general continue parsing + // -> hmmm we actually do this already!!! why is it not working? + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myAlternation, parsedElement.getGrammarElement()); +} + +// Not yet supported +//TEST(AlternationTest, OptionalChild) { +// FixedString child1("child1"); +// Optional child2; +// FixedString childOpt("child2"); +// child2.addChild(&childOpt); +// +// Alternation myAlternation; +// myAlternation.addChild(&child1); +// myAlternation.addChild(&child2); +// +// ParsedElement parent; +// ParsedElement parsedElement(&parent); +// +// ParseRc rc = myAlternation.parse("", parsedElement); +// +// // rc: +// EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); +// EXPECT_EQ(0, rc.lenParsedSuccessfully); +// +// // candidates: +// ASSERT_EQ(2, rc.candidates.size()); +// EXPECT_EQ("child1", rc.candidates[0]->getMatchedString()); +// EXPECT_EQ(&myAlternation, rc.candidates[0]->getGrammarElement()); +// EXPECT_EQ(&parent, rc.candidates[0]->getParent()); +// +// EXPECT_EQ("child2", rc.candidates[1]->getMatchedString()); +// EXPECT_EQ(&myAlternation, rc.candidates[1]->getGrammarElement()); +// EXPECT_EQ(&parent, rc.candidates[1]->getParent()); +// +// // parsedElement +// ASSERT_EQ(0, parsedElement.getChildren().size()); +// EXPECT_EQ(&parent, parsedElement.getParent()); +// EXPECT_EQ(false, parsedElement.isStopped()); +// EXPECT_EQ(nullptr, parsedElement.getGrammarElement()); +//} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..2225664 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,42 @@ +# Copyright 2019 IBM Corporation +# +# 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. + +cmake_minimum_required (VERSION 2.8) +project (gWhisper) + +include_directories("${PROJECT_BINARY_DIR}") + +set(TARGET_NAME "gwhisper_tests") +set(TARGET_SRC + FixedStringTest.cpp + ConcatenationTest.cpp + AlternationTest.cpp + RepetitionTest.cpp + GrammarComboTests.cpp + testmain.cpp + ) + +add_executable(${TARGET_NAME} ${TARGET_SRC}) + +target_link_libraries (${TARGET_NAME} + reflection + gtest + ) +if(BUILD_CONFIG_USE_BOOST_REGEX) + target_link_libraries (${TARGET_NAME} + boost_regex + ) +endif() + +add_test(NAME UnitTests COMMAND ${TARGET_NAME}) diff --git a/tests/ConcatenationTest.cpp b/tests/ConcatenationTest.cpp new file mode 100644 index 0000000..9af2321 --- /dev/null +++ b/tests/ConcatenationTest.cpp @@ -0,0 +1,350 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +using namespace ArgParse; + +// ----------------------------------------------------------------------------- +// Concatenation +// ----------------------------------------------------------------------------- + +TEST(ConcatenationTest, NoChildEmptyString) { + EXPECT_EQ(true, true); + + Concatenation myConcatenation; + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(ConcatenationTest, NoChildNonEmptyString) { + Concatenation myConcatenation; + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("test", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(ConcatenationTest, OneChildEmptyString) { + FixedString child1("child1"); + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + ASSERT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + ASSERT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(ConcatenationTest, TwoChildsCompleteMatch) { + FixedString child1("child1"); + FixedString child2("child2"); + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("child1child2asd", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(strlen("child1child2"), rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(2, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(ConcatenationTest, TwoChildsSecondIsPartialMatch) { + FixedString child1("child1"); + FixedString child2("child2"); + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("child1c", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(strlen("child1c"), rc.lenParsed); + EXPECT_EQ(strlen("child1"), rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child1child2", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(2, parsedElement.getChildren().size()); + EXPECT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("child1", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&child2, parsedElement.getChildren()[1]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[1]->getMatchedString()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(ConcatenationTest, TwoChildEmptyString) { + FixedString child1("child1"); + FixedString child2("child2"); + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child1child2", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(ConcatenationTest, TwoChildCorrectStartString) { + FixedString child1("child1"); + FixedString child2("child2"); + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("child", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child1child2", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(ConcatenationTest, TwoChildWrongString) { + FixedString child1("child1"); + FixedString child2("child2"); + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&child2); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("asd", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::unexpectedText, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(ConcatenationTest, SingleAlternationFork) { + FixedString child1("child1"); + Alternation alt; + FixedString altChild1("ac1"); + FixedString altChild2("ac2"); + alt.addChild(&altChild1); + alt.addChild(&altChild2); + + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&alt); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("child1ac1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("child1ac2", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(ConcatenationTest, SingleAlternationNoFork) { + FixedString child1("child1"); + Alternation alt; + FixedString altChild1("ac1"); + FixedString altChild2("ac2"); + alt.addChild(&altChild1); + alt.addChild(&altChild2); + + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&alt); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("", parsedElement, 0); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +// Not yet supported +//TEST(ConcatenationTest, OptionalChild) { +// FixedString child1("child1"); +// Optional child2; +// FixedString childOpt("child2"); +// child2.addChild(&childOpt); +// +// Concatenation myConcatenation; +// myConcatenation.addChild(&child1); +// myConcatenation.addChild(&child2); +// +// ParsedElement parent; +// ParsedElement parsedElement(&parent); +// +// ParseRc rc = myConcatenation.parse("", parsedElement); +// +// // rc: +// EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); +// EXPECT_EQ(0, rc.lenParsedSuccessfully); +// +// // candidates: +// ASSERT_EQ(2, rc.candidates.size()); +// EXPECT_EQ("child1", rc.candidates[0]->getMatchedString()); +// EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); +// EXPECT_EQ(&parent, rc.candidates[0]->getParent()); +// +// EXPECT_EQ("child2", rc.candidates[1]->getMatchedString()); +// EXPECT_EQ(&myConcatenation, rc.candidates[1]->getGrammarElement()); +// EXPECT_EQ(&parent, rc.candidates[1]->getParent()); +// +// // parsedElement +// ASSERT_EQ(0, parsedElement.getChildren().size()); +// EXPECT_EQ(&parent, parsedElement.getParent()); +// EXPECT_EQ(false, parsedElement.isStopped()); +// EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +//} diff --git a/tests/FixedStringTest.cpp b/tests/FixedStringTest.cpp new file mode 100644 index 0000000..272c07c --- /dev/null +++ b/tests/FixedStringTest.cpp @@ -0,0 +1,102 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +using namespace ArgParse; + +// ----------------------------------------------------------------------------- +// FixedString +// ----------------------------------------------------------------------------- + +TEST(FixedStringTest, EmptyFixedStringEmptyString) { + FixedString myFixedString(""); + ParsedElement parsedElement; + + ParseRc rc = myFixedString.parse("", parsedElement); + + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.candidates.size()); + EXPECT_EQ(0, rc.lenParsedSuccessfully); +} + +TEST(FixedStringTest, EmptyFixedStringNonEmptyString) { + FixedString myFixedString(""); + ParsedElement parsedElement; + + ParseRc rc = myFixedString.parse("test", parsedElement); + + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.candidates.size()); + EXPECT_EQ(0, rc.lenParsedSuccessfully); +} + +TEST(FixedStringTest, EmptyString) { + FixedString myFixedString("asd"); + ParsedElement parsedElement; + + ParseRc rc = myFixedString.parse("", parsedElement); + + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(1, rc.candidates.size()); + EXPECT_EQ("asd", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + EXPECT_EQ(&myFixedString, rc.candidates[0]->getGrammarElement()); +} + +TEST(FixedStringTest, MatchingString) { + FixedString myFixedString("asd"); + ParsedElement parsedElement; + + ParseRc rc = myFixedString.parse("asd", parsedElement); + + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(3, rc.lenParsedSuccessfully); + EXPECT_EQ(0, rc.candidates.size()); +} + +TEST(FixedStringTest, MatchingStringTooMuch) { + FixedString myFixedString("asd"); + ParsedElement parsedElement; + + ParseRc rc = myFixedString.parse("asdfgh", parsedElement); + + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(3, rc.lenParsedSuccessfully); + EXPECT_EQ(0, rc.candidates.size()); +} + +TEST(FixedStringTest, HalfMatchingString) { + FixedString myFixedString("asd"); + ParsedElement parsedElement; + + ParseRc rc = myFixedString.parse("a", parsedElement); + + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(1, rc.candidates.size()); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + EXPECT_EQ("asd", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myFixedString, rc.candidates[0]->getGrammarElement()); +} + +TEST(FixedStringTest, NonMatchingString) { + FixedString myFixedString("asd"); + ParsedElement parsedElement; + + ParseRc rc = myFixedString.parse("b", parsedElement); + + EXPECT_EQ(ParseRc::ErrorType::unexpectedText, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + EXPECT_EQ(0, rc.candidates.size()); +} diff --git a/tests/GrammarComboTests.cpp b/tests/GrammarComboTests.cpp new file mode 100644 index 0000000..48f35c8 --- /dev/null +++ b/tests/GrammarComboTests.cpp @@ -0,0 +1,1013 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +using namespace ArgParse; + +// ---------------------------------------------------------------------------- +// CA Combos +// ---------------------------------------------------------------------------- + +TEST(CA_ComboTest, CAC_combo) { + // c1 + // a1 + // f1 + // f2 + // c2 + // f3 + // f4 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + + Concatenation c1; + Concatenation c2; + + a1.addChild(&f1); + a1.addChild(&f2); + + c1.addChild(&a1); + c1.addChild(&c2); + + c2.addChild(&f3); + c2.addChild(&f4); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("f1f3f4", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("f2f3f4", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&a1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(0, parsedElement.getChildren()[0]->getChildren().size()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, CAC_combo_uniquePartialMatch) { + // c1 + // a1 + // f1 + // f2 + // c2 + // f3 + // f4 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + + Concatenation c1; + Concatenation c2; + + a1.addChild(&f1); + a1.addChild(&f2); + + c1.addChild(&a1); + c1.addChild(&c2); + + c2.addChild(&f3); + c2.addChild(&f4); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("f2", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType) << rc.toString(); + EXPECT_EQ(2, rc.lenParsed); + EXPECT_EQ(2, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("f2f3f4", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + // It is essential to have the first child here (otherwise grammar injection would not have access to parially parsed tree) + ASSERT_EQ(2, parsedElement.getChildren().size()); + EXPECT_EQ("f2", parsedElement.getMatchedString()); + + EXPECT_EQ(&a1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("f2", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(1, parsedElement.getChildren()[0]->getChildren().size()); + + EXPECT_EQ(&c2, parsedElement.getChildren()[1]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[1]->getMatchedString()); + EXPECT_EQ(1, parsedElement.getChildren()[1]->getChildren().size()); + EXPECT_EQ(&f3, parsedElement.getChildren()[1]->getChildren()[0]->getGrammarElement()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, CA_combo_partialMatch) { + // c1 + // a1 + // f1 + // f2 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + Concatenation c1; + + a1.addChild(&f1); + a1.addChild(&f2); + + c1.addChild(&a1); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("f", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(1, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("f1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("f2", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&a1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + + EXPECT_EQ(0, parsedElement.getChildren()[0]->getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, CA_combo_partialMatchFollowUpAlternation) { + // c1 + // a1 + // f1 + // f2 + // a2 + // f3 + // f4 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + Concatenation c1; + + a1.addChild(&f1); + a1.addChild(&f2); + + a2.addChild(&f3); + a2.addChild(&f4); + + c1.addChild(&a1); + c1.addChild(&a2); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("f", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(1, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("f1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("f2", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&a1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + + EXPECT_EQ(0, parsedElement.getChildren()[0]->getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, CCX_combo_noOverParse) { + // c1 + // c2 + // f1 + // x1 + // f3 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + RegEx x1("/asdfsadfsadf/"); + Concatenation c1; + Concatenation c2; + + c1.addChild(&c2); + c1.addChild(&f3); + + c2.addChild(&f1); + c2.addChild(&x1); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("f1", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(2, rc.lenParsed); + EXPECT_EQ(2, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&c2, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("f1", parsedElement.getChildren()[0]->getMatchedString()); + + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, CCX_combo_noOverParseUnexpectedText) { + // c1 + // c2 + // f1 + // x1 + // f3 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + RegEx x1("/asdfsadfsadf/"); + Concatenation c1; + Concatenation c2; + + c1.addChild(&c2); + c1.addChild(&f3); + + c2.addChild(&f1); + c2.addChild(&x1); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("f1h", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::unexpectedText, rc.errorType); + EXPECT_EQ(3, rc.lenParsed); + EXPECT_EQ(2, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, CCX_combo_noOverParsePartial) { + // c1 + // c2 + // f1 + // x1 + // f3 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + RegEx x1("/asdfsadfsadf/"); + Concatenation c1; + Concatenation c2; + + c1.addChild(&c2); + c1.addChild(&f3); + + c2.addChild(&f1); + c2.addChild(&x1); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("f", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(1, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("f1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, CA_combo) { + // c1 + // a1 + // f1 + // f2 + // f3 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + Concatenation c1; + + a1.addChild(&f1); + a1.addChild(&f2); + + c1.addChild(&a1); + c1.addChild(&f3); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("f1f3", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("f2f3", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&a1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + + EXPECT_EQ(0, parsedElement.getChildren()[0]->getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, ACACA_combo_NoFork) { + // a1 + // c1 + // f1 + // a2 + // f2 + // f3 + // c2 + // f4 + // a3 + // f5 + // f6 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + FixedString f6("f6"); + Alternation a1; + Alternation a2; + Alternation a3; + Concatenation c1; + Concatenation c2; + + a1.addChild(&c1); + a1.addChild(&c2); + + c1.addChild(&f1); + c1.addChild(&a2); + + c2.addChild(&f4); + c2.addChild(&a3); + + a2.addChild(&f2); + a2.addChild(&f3); + + a3.addChild(&f5); + a3.addChild(&f6); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = a1.parse("f", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(1, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("f1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&a1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("f4", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&a1, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&a1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, ACA_combo_NoFork) { + // a1 + // c1 + // f1 + // a2 + // f2 + // f3 + // f4 + // f5 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + Concatenation c1; + + a1.addChild(&c1); + a1.addChild(&f5); + + c1.addChild(&f1); + c1.addChild(&a2); + c1.addChild(&f4); + + a2.addChild(&f2); + a2.addChild(&f3); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = a1.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("f1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&a1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("f5", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&a1, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(0, parsedElement.getChildren().size()); // This might change to 1, if we enforce the "if it is unique, we add it" paradigm to the limit. I.e. if user has stopped inputting, we just continue parsing + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&a1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, ACA_combo_PartialMatchFork) { + // a1 + // c1 + // f1 + // a2 + // f2 + // f3 + // f4 + // f5 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Alternation a1; + Alternation a2; + Concatenation c1; + + a1.addChild(&c1); + a1.addChild(&f5); + + c1.addChild(&f1); + c1.addChild(&a2); + c1.addChild(&f4); + + a2.addChild(&f2); + a2.addChild(&f3); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = a1.parse("f1f", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(3, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("f1f2f4", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&a1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("f1f3f4", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&a1, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&a1, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, DoubleConcatWithAlternationFork) { + FixedString child1("f1"); + FixedString child2("f2"); + FixedString child3("f3"); + FixedString child4("f4"); + + Alternation alt; + alt.addChild(&child3); + alt.addChild(&child4); + + Concatenation c2; + c2.addChild(&child2); + c2.addChild(&alt); + + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&c2); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("f1f2f3", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("f1f2f4", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, RecursiveAlternationFork) { + FixedString child1("child1"); + + Alternation alt; + FixedString altChild1("ac1"); + FixedString altChild2("ac2"); + alt.addChild(&altChild1); + alt.addChild(&altChild2); + + Alternation altParent; + FixedString altChild3("ac3"); + altParent.addChild(&alt); + altParent.addChild(&altChild3); + + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&altParent); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(3, rc.candidates.size()); + EXPECT_EQ("child1ac1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("child1ac2", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + EXPECT_EQ("child1ac3", rc.candidates[2]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[2]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[2]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +TEST(CA_ComboTest, TwoAlternationNoFork) { + FixedString child1("child1"); + + Alternation alt1; + FixedString altChild1("ac1"); + FixedString altChild2("ac2"); + alt1.addChild(&altChild1); + alt1.addChild(&altChild2); + + Alternation alt2; + FixedString altChild3("ac3"); + FixedString altChild4("ac4"); + alt2.addChild(&altChild3); + alt2.addChild(&altChild4); + + Concatenation myConcatenation; + myConcatenation.addChild(&child1); + myConcatenation.addChild(&alt1); + myConcatenation.addChild(&alt2); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = myConcatenation.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(2, rc.candidates.size()); + EXPECT_EQ("child1ac1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + EXPECT_EQ("child1ac2", rc.candidates[1]->getMatchedString()); + EXPECT_EQ(&myConcatenation, rc.candidates[1]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[1]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&child1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&myConcatenation, parsedElement.getGrammarElement()); +} + +// ---------------------------------------------------------------------------- +// CR Combos +// ---------------------------------------------------------------------------- + +TEST(CR_ComboTest, CR_combo_Empty) { + // c1 + // r1 + // f1 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Repetition r1; + Concatenation c1; + + r1.addChild(&f1); + + c1.addChild(&r1); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("f1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&r1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(1, parsedElement.getChildren()[0]->getChildren().size()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CR_ComboTest, CR_combo_PartialMatchSandwitchVeryComplex) { + // c1 + // f1 + // r1 + // c2 + // f2 + // f4 + // f3 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3(":"); + FixedString f4("8"); + Repetition r1; + Concatenation c1; + Concatenation c2; + + r1.addChild(&c2); + + c1.addChild(&f1); + c1.addChild(&r1); + c1.addChild(&f3); + + c2.addChild(&f2); + c2.addChild(&f4); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("f1f2", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(4, rc.lenParsed); + EXPECT_EQ(4, rc.lenParsedSuccessfully); + + //EXPECT_EQ("f1f2faa2", rc.candidates[0]->getMatchedString()); + //EXPECT_EQ("f1f2faa2", rc.candidates[1]->getMatchedString()); + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("f1f28", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(2, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&f1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("f1", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&r1, parsedElement.getChildren()[1]->getGrammarElement()); + EXPECT_EQ("f2", parsedElement.getChildren()[1]->getMatchedString()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CR_ComboTest, CR_combo_PartialMatchSandwitch) { + // c1 + // f1 + // r1 + // f2 + // f3 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3(":"); + Repetition r1; + Concatenation c1; + + r1.addChild(&f2); + + c1.addChild(&f1); + c1.addChild(&r1); + c1.addChild(&f3); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("f1f2f", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(5, rc.lenParsed); + EXPECT_EQ(4, rc.lenParsedSuccessfully); + + //EXPECT_EQ("f1f2faa2", rc.candidates[0]->getMatchedString()); + //EXPECT_EQ("f1f2faa2", rc.candidates[1]->getMatchedString()); + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("f1f2f2", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(2, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&f1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("f1", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&r1, parsedElement.getChildren()[1]->getGrammarElement()); + EXPECT_EQ("f2", parsedElement.getChildren()[1]->getMatchedString()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CR_ComboTest, CR_combo_PartialMatch) { + // c1 + // r1 + // f1 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Repetition r1; + Concatenation c1; + + r1.addChild(&f1); + + c1.addChild(&r1); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = c1.parse("f", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(1, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("f1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&c1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&r1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(1, parsedElement.getChildren()[0]->getChildren().size()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&c1, parsedElement.getGrammarElement()); +} + +TEST(CR_ComboTest, CR_combo_RC_partialMatch) { + // r1 + // c1 + // f1 + + FixedString f1("f1"); + FixedString f2("f2"); + FixedString f3("f3"); + FixedString f4("f4"); + FixedString f5("f5"); + Repetition r1; + Concatenation c1; + + r1.addChild(&c1); + + c1.addChild(&f1); + + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = r1.parse("f", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(1, rc.lenParsed); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("f1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&r1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(&c1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(1, parsedElement.getChildren()[0]->getChildren().size()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&r1, parsedElement.getGrammarElement()); +} + diff --git a/tests/RepetitionTest.cpp b/tests/RepetitionTest.cpp new file mode 100644 index 0000000..11c137a --- /dev/null +++ b/tests/RepetitionTest.cpp @@ -0,0 +1,185 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +using namespace ArgParse; + +TEST(RepetitionTest, NoChildEmptyString) { + Repetition r1; + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = r1.parse("", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + EXPECT_EQ("", parsedElement.getMatchedString()); + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&r1, parsedElement.getGrammarElement()); +} + +TEST(RepetitionTest, NoChildFilledString) { + Repetition r1; + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = r1.parse("asd", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + EXPECT_EQ("", parsedElement.getMatchedString()); + ASSERT_EQ(0, parsedElement.getChildren().size()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&r1, parsedElement.getGrammarElement()); +} + + +TEST(RepetitionTest, ExactMatch) { + FixedString c1("child1"); + Repetition r1; + r1.addChild(&c1); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = r1.parse("child1", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(strlen("child1"), rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child1child1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&r1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + EXPECT_EQ("child1", parsedElement.getMatchedString()); + ASSERT_EQ(2, parsedElement.getChildren().size()); + EXPECT_EQ(&c1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("child1", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&c1, parsedElement.getChildren()[1]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[1]->getMatchedString()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&r1, parsedElement.getGrammarElement()); +} + + +TEST(RepetitionTest, MultipleMatch) { + FixedString c1("child1"); + Repetition r1; + r1.addChild(&c1); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = r1.parse("child1child1child1", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(strlen("child1child1child1"), rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child1child1child1child1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&r1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + EXPECT_EQ("child1child1child1", parsedElement.getMatchedString()); + ASSERT_EQ(4, parsedElement.getChildren().size()); + EXPECT_EQ(&c1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("child1", parsedElement.getChildren()[0]->getMatchedString()); + EXPECT_EQ(&c1, parsedElement.getChildren()[3]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[3]->getMatchedString()); + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&r1, parsedElement.getGrammarElement()); +} + +TEST(RepetitionTest, NoMatch) { + FixedString c1("child1"); + Repetition r1; + r1.addChild(&c1); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = r1.parse("asd", parsedElement); + + // rc: + EXPECT_EQ(ParseRc::ErrorType::success, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + // We do not expect any candidates, as we already have non matching text + ASSERT_EQ(0, rc.candidates.size()); + + // parsedElement + EXPECT_EQ("", parsedElement.getMatchedString()); + // also do not expect any parsed elements, as we could not uniquely identify + // any (partially) matching childs + ASSERT_EQ(0, parsedElement.getChildren().size()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&r1, parsedElement.getGrammarElement()); +} + +TEST(RepetitionTest, PartialMatch) { + FixedString c1("child1"); + Repetition r1; + r1.addChild(&c1); + ParsedElement parent; + ParsedElement parsedElement(&parent); + + ParseRc rc = r1.parse("ch", parsedElement); + + // rc: + // FIXME: what do want to have as RC here ??? we are not really expecting any more text, as a match of 0 would be fine... + EXPECT_EQ(ParseRc::ErrorType::missingText, rc.errorType); + EXPECT_EQ(0, rc.lenParsedSuccessfully); + + // candidates: + ASSERT_EQ(1, rc.candidates.size()); + EXPECT_EQ("child1", rc.candidates[0]->getMatchedString()); + EXPECT_EQ(&r1, rc.candidates[0]->getGrammarElement()); + EXPECT_EQ(&parent, rc.candidates[0]->getParent()); + + // parsedElement + EXPECT_EQ("", parsedElement.getMatchedString()); + ASSERT_EQ(1, parsedElement.getChildren().size()); + EXPECT_EQ(&c1, parsedElement.getChildren()[0]->getGrammarElement()); + EXPECT_EQ("", parsedElement.getChildren()[0]->getMatchedString()); + + EXPECT_EQ(&parent, parsedElement.getParent()); + EXPECT_EQ(false, parsedElement.isStopped()); + EXPECT_EQ(&r1, parsedElement.getGrammarElement()); +} + diff --git a/tests/testmain.cpp b/tests/testmain.cpp new file mode 100644 index 0000000..00f62a0 --- /dev/null +++ b/tests/testmain.cpp @@ -0,0 +1,20 @@ +// Copyright 2019 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt new file mode 100644 index 0000000..51bcf44 --- /dev/null +++ b/third_party/CMakeLists.txt @@ -0,0 +1,18 @@ +# Copyright 2019 IBM Corporation +# +# 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. + +cmake_minimum_required (VERSION 2.8) + +add_subdirectory(googletest) +add_subdirectory(gRPC_utils) diff --git a/third_party/gRPC_utils/CMakeLists.txt b/third_party/gRPC_utils/CMakeLists.txt new file mode 100644 index 0000000..35ff1e1 --- /dev/null +++ b/third_party/gRPC_utils/CMakeLists.txt @@ -0,0 +1,70 @@ +# Copyright 2019 IBM Corporation +# +# 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. + +cmake_minimum_required (VERSION 2.8) + +set(TARGET_NAME "reflection") +set(TARGET_SRC + proto_reflection_descriptor_database.cc + cli_call.cc + ) + +# find grpc + protobuf libs and code generators: +find_library(LIB_PROTOBUF protobuf) +find_library(LIB_GRPC grpc) +find_library(LIB_GRPC++ grpc++) +find_library(LIB_GRPC++_reflection grpc++_reflection) +find_program (PROTOC protoc) +find_program (PROTOC_GRPC_PLUGIN grpc_cpp_plugin) +set(GRPC_LIBS_REFLECTION -Wl,--no-as-needed ${LIB_GRPC++_reflection} -Wl,--as-needed ${LIB_GRPC++} ${LIB_GRPC} ${LIB_PROTOBUF}) +message(STATUS "PROTOC = ${PROTOC}") +message(STATUS "PROTOC_GRPC_PLUGIN = ${PROTOC_GRPC_PLUGIN}") +message(STATUS "DYNAMIC GRPC LINKING INFO = ${GRPC_LIBS_REFLECTION}") + +# determine proto file source and binary directories (binary directory used to +# write generated code to) +#set(PROTO_FILE_BASE_SRC_PATH ${CMAKE_SOURCE_DIR}/third_party) +set(PROTO_FILE_BASE_SRC_PATH ${CMAKE_CURRENT_SOURCE_DIR}) +string(REGEX REPLACE "^${CMAKE_SOURCE_DIR}" "${CMAKE_BINARY_DIR}" PROTO_FILE_BASE_DST_PATH ${PROTO_FILE_BASE_SRC_PATH}) +message(STATUS "PROTO_FILE_BASE_SRC_PATH = ${PROTO_FILE_BASE_SRC_PATH}") +message(STATUS "PROTO_FILE_BASE_DST_PATH = ${PROTO_FILE_BASE_DST_PATH}") + +# add reflection generated code location to include path: +include_directories(${PROTO_FILE_BASE_DST_PATH}) + +# add rules for code generation: +add_custom_command( + OUTPUT reflection.pb.cc reflection.pb.hh + COMMAND ${PROTOC} -I${PROTO_FILE_BASE_SRC_PATH} --cpp_out=${PROTO_FILE_BASE_DST_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/reflection.proto + DEPENDS reflection.proto + ) +add_custom_command( + OUTPUT reflection.grpc.pb.cc reflection.grpc.pb.hh + COMMAND ${PROTOC} -I${PROTO_FILE_BASE_SRC_PATH} --grpc_out=${PROTO_FILE_BASE_DST_PATH} --plugin=protoc-gen-grpc=${PROTOC_GRPC_PLUGIN} ${CMAKE_CURRENT_SOURCE_DIR}/reflection.proto + DEPENDS reflection.proto + ) + +add_library(${TARGET_NAME} ${TARGET_SRC} + # NOTE: it is very important to list the headers here and not as dependencies + # otherwise with CMAKE 2.8 we code generation will not be triggered + reflection.pb.hh + reflection.grpc.pb.hh + ) + +# NOTE: we only need the reflection.proto generated headers here, and do not need +# to link against generated source files, as grpc libs seem to already +# bring the reflection lib +target_link_libraries(${TARGET_NAME} + ${GRPC_LIBS_REFLECTION} + ) diff --git a/third_party/gRPC_utils/LICENSE b/third_party/gRPC_utils/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/third_party/gRPC_utils/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/third_party/gRPC_utils/README b/third_party/gRPC_utils/README new file mode 100644 index 0000000..35bf494 --- /dev/null +++ b/third_party/gRPC_utils/README @@ -0,0 +1,7 @@ +Files in this directory were taken from the official gRPC repository (https://github.com/grpc/grpc). + +They include the RPC reflection interface description, as well as +very useful gRPC RPC call and reflection abtractions. + +Some minor modifications were made to those files to allow building them +in this projects (include paths). Those modifications are clearly marked. diff --git a/third_party/gRPC_utils/cli_call.cc b/third_party/gRPC_utils/cli_call.cc new file mode 100644 index 0000000..1b3d1b2 --- /dev/null +++ b/third_party/gRPC_utils/cli_call.cc @@ -0,0 +1,214 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + * + */ + +// This line was modified by Rainer Schoenberger IBM +//#include "test/cpp/util/cli_call.h" +#include "cli_call.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace grpc { +namespace testing { +namespace { +void* tag(int i) { return (void*)static_cast(i); } +} // namespace + +Status CliCall::Call(std::shared_ptr channel, + const grpc::string& method, const grpc::string& request, + grpc::string* response, + const OutgoingMetadataContainer& metadata, + IncomingMetadataContainer* server_initial_metadata, + IncomingMetadataContainer* server_trailing_metadata) { + CliCall call(channel, method, metadata); + call.Write(request); + call.WritesDone(); + if (!call.Read(response, server_initial_metadata)) { + fprintf(stderr, "Failed to read response.\n"); + } + return call.Finish(server_trailing_metadata); +} + +CliCall::CliCall(std::shared_ptr channel, + const grpc::string& method, + const OutgoingMetadataContainer& metadata) + : stub_(new grpc::GenericStub(channel)) { + gpr_mu_init(&write_mu_); + gpr_cv_init(&write_cv_); + if (!metadata.empty()) { + for (OutgoingMetadataContainer::const_iterator iter = metadata.begin(); + iter != metadata.end(); ++iter) { + ctx_.AddMetadata(iter->first, iter->second); + } + } + call_ = stub_->PrepareCall(&ctx_, method, &cq_); + call_->StartCall(tag(1)); + void* got_tag; + bool ok; + cq_.Next(&got_tag, &ok); + GPR_ASSERT(ok); +} + +CliCall::~CliCall() { + gpr_cv_destroy(&write_cv_); + gpr_mu_destroy(&write_mu_); +} + +void CliCall::Write(const grpc::string& request) { + void* got_tag; + bool ok; + + gpr_slice s = gpr_slice_from_copied_buffer(request.data(), request.size()); + grpc::Slice req_slice(s, grpc::Slice::STEAL_REF); + grpc::ByteBuffer send_buffer(&req_slice, 1); + call_->Write(send_buffer, tag(2)); + cq_.Next(&got_tag, &ok); + GPR_ASSERT(ok); +} + +bool CliCall::Read(grpc::string* response, + IncomingMetadataContainer* server_initial_metadata) { + void* got_tag; + bool ok; + + grpc::ByteBuffer recv_buffer; + call_->Read(&recv_buffer, tag(3)); + + if (!cq_.Next(&got_tag, &ok) || !ok) { + return false; + } + std::vector slices; + GPR_ASSERT(recv_buffer.Dump(&slices).ok()); + + response->clear(); + for (size_t i = 0; i < slices.size(); i++) { + response->append(reinterpret_cast(slices[i].begin()), + slices[i].size()); + } + if (server_initial_metadata) { + *server_initial_metadata = ctx_.GetServerInitialMetadata(); + } + return true; +} + +void CliCall::WritesDone() { + void* got_tag; + bool ok; + + call_->WritesDone(tag(4)); + cq_.Next(&got_tag, &ok); + GPR_ASSERT(ok); +} + +void CliCall::WriteAndWait(const grpc::string& request) { + grpc::Slice req_slice(request); + grpc::ByteBuffer send_buffer(&req_slice, 1); + + gpr_mu_lock(&write_mu_); + call_->Write(send_buffer, tag(2)); + write_done_ = false; + while (!write_done_) { + gpr_cv_wait(&write_cv_, &write_mu_, gpr_inf_future(GPR_CLOCK_MONOTONIC)); + } + gpr_mu_unlock(&write_mu_); +} + +void CliCall::WritesDoneAndWait() { + gpr_mu_lock(&write_mu_); + call_->WritesDone(tag(4)); + write_done_ = false; + while (!write_done_) { + gpr_cv_wait(&write_cv_, &write_mu_, gpr_inf_future(GPR_CLOCK_MONOTONIC)); + } + gpr_mu_unlock(&write_mu_); +} + +bool CliCall::ReadAndMaybeNotifyWrite( + grpc::string* response, + IncomingMetadataContainer* server_initial_metadata) { + void* got_tag; + bool ok; + grpc::ByteBuffer recv_buffer; + + call_->Read(&recv_buffer, tag(3)); + bool cq_result = cq_.Next(&got_tag, &ok); + + while (got_tag != tag(3)) { + gpr_mu_lock(&write_mu_); + write_done_ = true; + gpr_cv_signal(&write_cv_); + gpr_mu_unlock(&write_mu_); + + cq_result = cq_.Next(&got_tag, &ok); + if (got_tag == tag(2)) { + GPR_ASSERT(ok); + } + } + + if (!cq_result || !ok) { + // If the RPC is ended on the server side, we should still wait for the + // pending write on the client side to be done. + if (!ok) { + gpr_mu_lock(&write_mu_); + if (!write_done_) { + cq_.Next(&got_tag, &ok); + GPR_ASSERT(got_tag != tag(2)); + write_done_ = true; + gpr_cv_signal(&write_cv_); + } + gpr_mu_unlock(&write_mu_); + } + return false; + } + + std::vector slices; + GPR_ASSERT(recv_buffer.Dump(&slices).ok()); + response->clear(); + for (size_t i = 0; i < slices.size(); i++) { + response->append(reinterpret_cast(slices[i].begin()), + slices[i].size()); + } + if (server_initial_metadata) { + *server_initial_metadata = ctx_.GetServerInitialMetadata(); + } + return true; +} + +Status CliCall::Finish(IncomingMetadataContainer* server_trailing_metadata) { + void* got_tag; + bool ok; + grpc::Status status; + + call_->Finish(&status, tag(5)); + cq_.Next(&got_tag, &ok); + GPR_ASSERT(ok); + if (server_trailing_metadata) { + *server_trailing_metadata = ctx_.GetServerTrailingMetadata(); + } + + return status; +} + +} // namespace testing +} // namespace grpc diff --git a/third_party/gRPC_utils/cli_call.h b/third_party/gRPC_utils/cli_call.h new file mode 100644 index 0000000..51ffafd --- /dev/null +++ b/third_party/gRPC_utils/cli_call.h @@ -0,0 +1,98 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + * + */ + +#ifndef GRPC_TEST_CPP_UTIL_CLI_CALL_H +#define GRPC_TEST_CPP_UTIL_CLI_CALL_H + +#include + +#include +#include +#include +#include +#include + +namespace grpc { + +class ClientContext; + +namespace testing { + +// CliCall handles the sending and receiving of generic messages given the name +// of the remote method. This class is only used by GrpcTool. Its thread-safe +// and thread-unsafe methods should not be used together. +class CliCall final { + public: + typedef std::multimap OutgoingMetadataContainer; + typedef std::multimap + IncomingMetadataContainer; + + CliCall(std::shared_ptr channel, const grpc::string& method, + const OutgoingMetadataContainer& metadata); + ~CliCall(); + + // Perform an unary generic RPC. + static Status Call(std::shared_ptr channel, + const grpc::string& method, const grpc::string& request, + grpc::string* response, + const OutgoingMetadataContainer& metadata, + IncomingMetadataContainer* server_initial_metadata, + IncomingMetadataContainer* server_trailing_metadata); + + // Send a generic request message in a synchronous manner. NOT thread-safe. + void Write(const grpc::string& request); + + // Send a generic request message in a synchronous manner. NOT thread-safe. + void WritesDone(); + + // Receive a generic response message in a synchronous manner.NOT thread-safe. + bool Read(grpc::string* response, + IncomingMetadataContainer* server_initial_metadata); + + // Thread-safe write. Must be used with ReadAndMaybeNotifyWrite. Send out a + // generic request message and wait for ReadAndMaybeNotifyWrite to finish it. + void WriteAndWait(const grpc::string& request); + + // Thread-safe WritesDone. Must be used with ReadAndMaybeNotifyWrite. Send out + // WritesDone for gereneric request messages and wait for + // ReadAndMaybeNotifyWrite to finish it. + void WritesDoneAndWait(); + + // Thread-safe Read. Blockingly receive a generic response message. Notify + // writes if they are finished when this read is waiting for a resposne. + bool ReadAndMaybeNotifyWrite( + grpc::string* response, + IncomingMetadataContainer* server_initial_metadata); + + // Finish the RPC. + Status Finish(IncomingMetadataContainer* server_trailing_metadata); + + private: + std::unique_ptr stub_; + grpc::ClientContext ctx_; + std::unique_ptr call_; + grpc::CompletionQueue cq_; + gpr_mu write_mu_; + gpr_cv write_cv_; // Protected by write_mu_; + bool write_done_; // Portected by write_mu_; +}; + +} // namespace testing +} // namespace grpc + +#endif // GRPC_TEST_CPP_UTIL_CLI_CALL_H diff --git a/third_party/gRPC_utils/proto_reflection_descriptor_database.cc b/third_party/gRPC_utils/proto_reflection_descriptor_database.cc new file mode 100644 index 0000000..d372765 --- /dev/null +++ b/third_party/gRPC_utils/proto_reflection_descriptor_database.cc @@ -0,0 +1,333 @@ +/* + * + * Copyright 2016 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + * + */ + +// MODIFIED by IBM (Rainer Schoenberger) +// original: #include "test/cpp/util/proto_reflection_descriptor_database.h" +#include "proto_reflection_descriptor_database.h" +// END MODIFIED + +#include + +#include + +using grpc::reflection::v1alpha::ErrorResponse; +using grpc::reflection::v1alpha::ListServiceResponse; +using grpc::reflection::v1alpha::ServerReflection; +using grpc::reflection::v1alpha::ServerReflectionRequest; +using grpc::reflection::v1alpha::ServerReflectionResponse; + +namespace grpc { + +ProtoReflectionDescriptorDatabase::ProtoReflectionDescriptorDatabase( + std::unique_ptr stub) + : stub_(std::move(stub)) {} + +ProtoReflectionDescriptorDatabase::ProtoReflectionDescriptorDatabase( + std::shared_ptr channel) + : stub_(ServerReflection::NewStub(channel)) {} + +ProtoReflectionDescriptorDatabase::~ProtoReflectionDescriptorDatabase() { + if (stream_) { + stream_->WritesDone(); + Status status = stream_->Finish(); + if (!status.ok()) { + if (status.error_code() == StatusCode::UNIMPLEMENTED) { + gpr_log(GPR_INFO, + "Reflection request not implemented; " + "is the ServerReflection service enabled?"); + } + gpr_log(GPR_INFO, + "ServerReflectionInfo rpc failed. Error code: %d, details: %s", + static_cast(status.error_code()), + status.error_message().c_str()); + } + } +} + +bool ProtoReflectionDescriptorDatabase::FindFileByName( + const string& filename, protobuf::FileDescriptorProto* output) { + if (cached_db_.FindFileByName(filename, output)) { + return true; + } + + if (known_files_.find(filename) != known_files_.end()) { + return false; + } + + ServerReflectionRequest request; + request.set_file_by_filename(filename); + ServerReflectionResponse response; + + if (!DoOneRequest(request, response)) { + return false; + } + + if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase::kFileDescriptorResponse) { + AddFileFromResponse(response.file_descriptor_response()); + } else if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase::kErrorResponse) { + const ErrorResponse error = response.error_response(); + if (error.error_code() == StatusCode::NOT_FOUND) { + gpr_log(GPR_INFO, "NOT_FOUND from server for FindFileByName(%s)", + filename.c_str()); + } else { + gpr_log(GPR_INFO, + "Error on FindFileByName(%s)\n\tError code: %d\n" + "\tError Message: %s", + filename.c_str(), error.error_code(), + error.error_message().c_str()); + } + } else { + gpr_log( + GPR_INFO, + "Error on FindFileByName(%s) response type\n" + "\tExpecting: %d\n\tReceived: %d", + filename.c_str(), + ServerReflectionResponse::MessageResponseCase::kFileDescriptorResponse, + response.message_response_case()); + } + + return cached_db_.FindFileByName(filename, output); +} + +bool ProtoReflectionDescriptorDatabase::FindFileContainingSymbol( + const string& symbol_name, protobuf::FileDescriptorProto* output) { + if (cached_db_.FindFileContainingSymbol(symbol_name, output)) { + return true; + } + + if (missing_symbols_.find(symbol_name) != missing_symbols_.end()) { + return false; + } + + ServerReflectionRequest request; + request.set_file_containing_symbol(symbol_name); + ServerReflectionResponse response; + + if (!DoOneRequest(request, response)) { + return false; + } + + if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase::kFileDescriptorResponse) { + AddFileFromResponse(response.file_descriptor_response()); + } else if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase::kErrorResponse) { + const ErrorResponse error = response.error_response(); + if (error.error_code() == StatusCode::NOT_FOUND) { + missing_symbols_.insert(symbol_name); + gpr_log(GPR_INFO, + "NOT_FOUND from server for FindFileContainingSymbol(%s)", + symbol_name.c_str()); + } else { + gpr_log(GPR_INFO, + "Error on FindFileContainingSymbol(%s)\n" + "\tError code: %d\n\tError Message: %s", + symbol_name.c_str(), error.error_code(), + error.error_message().c_str()); + } + } else { + gpr_log( + GPR_INFO, + "Error on FindFileContainingSymbol(%s) response type\n" + "\tExpecting: %d\n\tReceived: %d", + symbol_name.c_str(), + ServerReflectionResponse::MessageResponseCase::kFileDescriptorResponse, + response.message_response_case()); + } + return cached_db_.FindFileContainingSymbol(symbol_name, output); +} + +bool ProtoReflectionDescriptorDatabase::FindFileContainingExtension( + const string& containing_type, int field_number, + protobuf::FileDescriptorProto* output) { + if (cached_db_.FindFileContainingExtension(containing_type, field_number, + output)) { + return true; + } + + if (missing_extensions_.find(containing_type) != missing_extensions_.end() && + missing_extensions_[containing_type].find(field_number) != + missing_extensions_[containing_type].end()) { + gpr_log(GPR_INFO, "nested map."); + return false; + } + + ServerReflectionRequest request; + request.mutable_file_containing_extension()->set_containing_type( + containing_type); + request.mutable_file_containing_extension()->set_extension_number( + field_number); + ServerReflectionResponse response; + + if (!DoOneRequest(request, response)) { + return false; + } + + if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase::kFileDescriptorResponse) { + AddFileFromResponse(response.file_descriptor_response()); + } else if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase::kErrorResponse) { + const ErrorResponse error = response.error_response(); + if (error.error_code() == StatusCode::NOT_FOUND) { + if (missing_extensions_.find(containing_type) == + missing_extensions_.end()) { + missing_extensions_[containing_type] = {}; + } + missing_extensions_[containing_type].insert(field_number); + gpr_log(GPR_INFO, + "NOT_FOUND from server for FindFileContainingExtension(%s, %d)", + containing_type.c_str(), field_number); + } else { + gpr_log(GPR_INFO, + "Error on FindFileContainingExtension(%s, %d)\n" + "\tError code: %d\n\tError Message: %s", + containing_type.c_str(), field_number, error.error_code(), + error.error_message().c_str()); + } + } else { + gpr_log( + GPR_INFO, + "Error on FindFileContainingExtension(%s, %d) response type\n" + "\tExpecting: %d\n\tReceived: %d", + containing_type.c_str(), field_number, + ServerReflectionResponse::MessageResponseCase::kFileDescriptorResponse, + response.message_response_case()); + } + + return cached_db_.FindFileContainingExtension(containing_type, field_number, + output); +} + +bool ProtoReflectionDescriptorDatabase::FindAllExtensionNumbers( + const string& extendee_type, std::vector* output) { + if (cached_extension_numbers_.find(extendee_type) != + cached_extension_numbers_.end()) { + *output = cached_extension_numbers_[extendee_type]; + return true; + } + + ServerReflectionRequest request; + request.set_all_extension_numbers_of_type(extendee_type); + ServerReflectionResponse response; + + if (!DoOneRequest(request, response)) { + return false; + } + + if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase:: + kAllExtensionNumbersResponse) { + auto number = response.all_extension_numbers_response().extension_number(); + *output = std::vector(number.begin(), number.end()); + cached_extension_numbers_[extendee_type] = *output; + return true; + } else if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase::kErrorResponse) { + const ErrorResponse error = response.error_response(); + if (error.error_code() == StatusCode::NOT_FOUND) { + gpr_log(GPR_INFO, "NOT_FOUND from server for FindAllExtensionNumbers(%s)", + extendee_type.c_str()); + } else { + gpr_log(GPR_INFO, + "Error on FindAllExtensionNumbersExtension(%s)\n" + "\tError code: %d\n\tError Message: %s", + extendee_type.c_str(), error.error_code(), + error.error_message().c_str()); + } + } + return false; +} + +bool ProtoReflectionDescriptorDatabase::GetServices( + std::vector* output) { + ServerReflectionRequest request; + request.set_list_services(""); + ServerReflectionResponse response; + + if (!DoOneRequest(request, response)) { + return false; + } + + if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase::kListServicesResponse) { + const ListServiceResponse ls_response = response.list_services_response(); + for (int i = 0; i < ls_response.service_size(); ++i) { + (*output).push_back(ls_response.service(i).name()); + } + return true; + } else if (response.message_response_case() == + ServerReflectionResponse::MessageResponseCase::kErrorResponse) { + const ErrorResponse error = response.error_response(); + gpr_log(GPR_INFO, + "Error on GetServices()\n\tError code: %d\n" + "\tError Message: %s", + error.error_code(), error.error_message().c_str()); + } else { + gpr_log( + GPR_INFO, + "Error on GetServices() response type\n\tExpecting: %d\n\tReceived: %d", + ServerReflectionResponse::MessageResponseCase::kListServicesResponse, + response.message_response_case()); + } + return false; +} + +const protobuf::FileDescriptorProto +ProtoReflectionDescriptorDatabase::ParseFileDescriptorProtoResponse( + const grpc::string& byte_fd_proto) { + protobuf::FileDescriptorProto file_desc_proto; + file_desc_proto.ParseFromString(byte_fd_proto); + return file_desc_proto; +} + +void ProtoReflectionDescriptorDatabase::AddFileFromResponse( + const grpc::reflection::v1alpha::FileDescriptorResponse& response) { + for (int i = 0; i < response.file_descriptor_proto_size(); ++i) { + const protobuf::FileDescriptorProto file_proto = + ParseFileDescriptorProtoResponse(response.file_descriptor_proto(i)); + if (known_files_.find(file_proto.name()) == known_files_.end()) { + known_files_.insert(file_proto.name()); + cached_db_.Add(file_proto); + } + } +} + +const std::shared_ptr +ProtoReflectionDescriptorDatabase::GetStream() { + if (!stream_) { + stream_ = stub_->ServerReflectionInfo(&ctx_); + } + return stream_; +} + +bool ProtoReflectionDescriptorDatabase::DoOneRequest( + const ServerReflectionRequest& request, + ServerReflectionResponse& response) { + bool success = false; + stream_mutex_.lock(); + if (GetStream()->Write(request) && GetStream()->Read(&response)) { + success = true; + } + stream_mutex_.unlock(); + return success; +} + +} // namespace grpc diff --git a/third_party/gRPC_utils/proto_reflection_descriptor_database.h b/third_party/gRPC_utils/proto_reflection_descriptor_database.h new file mode 100644 index 0000000..0dd3784 --- /dev/null +++ b/third_party/gRPC_utils/proto_reflection_descriptor_database.h @@ -0,0 +1,114 @@ +/* + * + * Copyright 2016 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + * + */ +#ifndef GRPC_TEST_CPP_PROTO_SERVER_REFLECTION_DATABSE_H +#define GRPC_TEST_CPP_PROTO_SERVER_REFLECTION_DATABSE_H + +#include +#include +#include +#include + +#include +#include +// MODIFIED by IBM (Rainer Schoenberger) +// original: #include "src/proto/grpc/reflection/v1alpha/reflection.grpc.pb.h" +#include "third_party/gRPC_utils/reflection.grpc.pb.h" +// END MODIFIED + +namespace grpc { + +// ProtoReflectionDescriptorDatabase takes a stub of ServerReflection and +// provides the methods defined by DescriptorDatabase interfaces. It can be used +// to feed a DescriptorPool instance. +class ProtoReflectionDescriptorDatabase : public protobuf::DescriptorDatabase { + public: + explicit ProtoReflectionDescriptorDatabase( + std::unique_ptr stub); + + explicit ProtoReflectionDescriptorDatabase( + std::shared_ptr channel); + + virtual ~ProtoReflectionDescriptorDatabase(); + + // The following four methods implement DescriptorDatabase interfaces. + // + // Find a file by file name. Fills in in *output and returns true if found. + // Otherwise, returns false, leaving the contents of *output undefined. + bool FindFileByName(const string& filename, + protobuf::FileDescriptorProto* output) override; + + // Find the file that declares the given fully-qualified symbol name. + // If found, fills in *output and returns true, otherwise returns false + // and leaves *output undefined. + bool FindFileContainingSymbol(const string& symbol_name, + protobuf::FileDescriptorProto* output) override; + + // Find the file which defines an extension extending the given message type + // with the given field number. If found, fills in *output and returns true, + // otherwise returns false and leaves *output undefined. containing_type + // must be a fully-qualified type name. + bool FindFileContainingExtension( + const string& containing_type, int field_number, + protobuf::FileDescriptorProto* output) override; + + // Finds the tag numbers used by all known extensions of + // extendee_type, and appends them to output in an undefined + // order. This method is best-effort: it's not guaranteed that the + // database will find all extensions, and it's not guaranteed that + // FindFileContainingExtension will return true on all of the found + // numbers. Returns true if the search was successful, otherwise + // returns false and leaves output unchanged. + bool FindAllExtensionNumbers(const string& extendee_type, + std::vector* output) override; + + // Provide a list of full names of registered services + bool GetServices(std::vector* output); + + private: + typedef ClientReaderWriter< + grpc::reflection::v1alpha::ServerReflectionRequest, + grpc::reflection::v1alpha::ServerReflectionResponse> + ClientStream; + + const protobuf::FileDescriptorProto ParseFileDescriptorProtoResponse( + const grpc::string& byte_fd_proto); + + void AddFileFromResponse( + const grpc::reflection::v1alpha::FileDescriptorResponse& response); + + const std::shared_ptr GetStream(); + + bool DoOneRequest( + const grpc::reflection::v1alpha::ServerReflectionRequest& request, + grpc::reflection::v1alpha::ServerReflectionResponse& response); + + std::shared_ptr stream_; + grpc::ClientContext ctx_; + std::unique_ptr stub_; + std::unordered_set known_files_; + std::unordered_set missing_symbols_; + std::unordered_map> missing_extensions_; + std::unordered_map> cached_extension_numbers_; + std::mutex stream_mutex_; + + protobuf::SimpleDescriptorDatabase cached_db_; +}; + +} // namespace grpc + +#endif // GRPC_TEST_CPP_METRICS_SERVER_H diff --git a/third_party/gRPC_utils/reflection.proto b/third_party/gRPC_utils/reflection.proto new file mode 100644 index 0000000..816852f --- /dev/null +++ b/third_party/gRPC_utils/reflection.proto @@ -0,0 +1,136 @@ +// Copyright 2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// 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. + +// Service exported by server reflection + +syntax = "proto3"; + +package grpc.reflection.v1alpha; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server set one of the following fields accroding to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. As + // the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requst. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services request. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} diff --git a/third_party/googletest b/third_party/googletest new file mode 160000 index 0000000..8b6d3f9 --- /dev/null +++ b/third_party/googletest @@ -0,0 +1 @@ +Subproject commit 8b6d3f9c4a774bef3081195d422993323b6bb2e0