diff --git a/coverage.sh b/coverage.sh index 3020f10..5b8bd43 100755 --- a/coverage.sh +++ b/coverage.sh @@ -45,7 +45,7 @@ OUTDIR="$PWD"/coverage install -d "$OUTDIR" # pre-process for each directory, correcting "file" relative to repo root -for dir in src ioc +for dir in src ioc tools do ( cd "$TDIR"/$dir/O.linux-* \ && gcovr --gcov-ignore-parse-errors -v -r .. --json \ @@ -57,6 +57,7 @@ done gcovr \ --add-tracefile "$TDIR"/src.json \ --add-tracefile "$TDIR"/ioc.json \ + --add-tracefile "$TDIR"/tools.json \ --html --html-details -o "$OUTDIR"/coverage.html cd "$OUTDIR" diff --git a/tools/Makefile b/tools/Makefile index 9c885d2..6a72e5c 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -12,6 +12,8 @@ USR_CPPFLAGS += -I$(TOP)/src PROD_LIBS += pvxs Com +PROD_SRCS += cliutil.cpp + PROD += pvxvct pvxvct_SRCS += pvxvct.cpp @@ -36,6 +38,18 @@ pvxlist_SRCS += list.cpp PROD += pvxmshim pvxmshim_SRCS += mshim.cpp +# tests of CLI tools + +TESTPROD_HOST += testcliutil +testcliutil_SRCS += testcliutil.cpp +TESTS += testcliutil + +TESTPROD_HOST += testxput +testxput_SRCS += testxput.cpp +TESTS += testxput + +TESTSCRIPTS_HOST += $(TESTS:%=%.t) + endif # BASE_3_15 #=========================== diff --git a/tools/cliutil.cpp b/tools/cliutil.cpp new file mode 100644 index 0000000..d5a1563 --- /dev/null +++ b/tools/cliutil.cpp @@ -0,0 +1,83 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include "cliutil.h" + +namespace pvxs { + +bool operator==(const ArgVal& rhs, const ArgVal& lhs) { + return rhs.defined==lhs.defined && rhs.value==lhs.value; +} + +GetOpt::GetOpt(int argc, char *argv[], const char *spec) + :argv0("") +{ + if(argc>=1) + argv0 = argv[0]; + + bool allpos = false; // after "--", treat all remaining as positional + for(int i=1; i +#include +#include +#include // for std::pair + +#include + +namespace pvxs { + +struct ArgVal { + std::string value; + bool defined = false; + + ArgVal() = default; + ArgVal(std::nullptr_t) {} + ArgVal(const std::string& value) :value(value), defined(true) {} + ArgVal(const char* value) :value(value), defined(true) {} + + inline explicit + operator bool() const { return defined; } + + inline + const std::string& operator*() const { + if(defined) + return value; + throw std::logic_error("Undefined argument value"); + } + + template + inline + V as() const { + return parseTo(**this); + } +}; + +bool operator==(const ArgVal& rhs, const ArgVal& lhs); + +struct GetOpt { + GetOpt(int argc, char *argv[], const char *spec); + + const char *argv0; + std::vector positional; + std::vector> arguments; + bool success = false; +}; + +} // namespace pvxs + +#endif // CLIUTIL_H diff --git a/tools/put.cpp b/tools/put.cpp index 8685c54..a7cc8d5 100644 --- a/tools/put.cpp +++ b/tools/put.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #define PVXS_ENABLE_EXPERT_API @@ -19,6 +18,11 @@ #include #include "utilpvt.h" #include "evhelper.h" +#include "cliutil.h" + +#ifndef REALMAIN +# define REALMAIN main +#endif using namespace pvxs; @@ -37,9 +41,9 @@ void usage(const char* argv0) ; } -} +} // namespace -int main(int argc, char *argv[]) +int REALMAIN(int argc, char *argv[]) { try { logger_config_env(); // from $PVXS_LOG @@ -47,55 +51,53 @@ int main(int argc, char *argv[]) bool verbose = false; std::string request; - { - int opt; - while ((opt = getopt(argc, argv, "hvVdw:r:")) != -1) { - switch(opt) { - case 'h': - usage(argv[0]); - return 0; - case 'V': - std::cout<(optarg); - break; - case 'r': - request = optarg; - break; - default: - usage(argv[0]); - std::cerr<<"\nUnknown argument: "<(); + break; + case 'r': + request = *pair.second; + break; + default: + usage(opts.argv0); + std::cerr<<"\nUnknown argument: "< values; - if(argc-optind==1 && std::string(argv[optind]).find_first_of('=')==std::string::npos) { + if(opts.positional.size()==2 && std::string(opts.positional[1]).find_first_of('=')==std::string::npos) { // only one field assignment, and field name omitted. // if JSON map, treat as entire struct. Others imply .value - auto sval(argv[optind]); + const auto& sval = opts.positional[1]; values[sval[0]=='{' ? "" : "value"] = sval; } else { - for(auto n : range(optind, argc)) { - std::string fv(argv[n]); + for(auto n : range(size_t(1), opts.positional.size())) { + std::string fv(opts.positional[n]); auto sep = fv.find_first_of('='); if(sep==std::string::npos) { diff --git a/tools/testcliutil.cpp b/tools/testcliutil.cpp new file mode 100644 index 0000000..aff24c3 --- /dev/null +++ b/tools/testcliutil.cpp @@ -0,0 +1,143 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include "cliutil.h" + +namespace pvxs { namespace detail { + +template +struct test_print> { + template + static inline void op(C& strm, const std::vector& v) { + bool first = true; + for(auto& e : v) { + if(first) { + first = false; + } else { + strm<<' '; + } + test_print::op(strm, e); + } + } +}; + +template<> +struct test_print> { + template + static inline void op(C& strm, const std::pair& v) { + strm<<'-'<(argv), "a:v"); + testOk(strcmp(opts.argv0, "exe")==0, "%s", opts.argv0); + decltype (opts.arguments) arguments({{'v', nullptr}, {'a',"Aa"}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-v", "-a", "Aa", "hello"}; + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testOk(strcmp(opts.argv0, "exe")==0, "%s", opts.argv0); + decltype (opts.arguments) arguments({{'v', nullptr}, {'a',"Aa"}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-v", "hello", "-aAa"}; + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testOk(strcmp(opts.argv0, "exe")==0, "%s", opts.argv0); + decltype (opts.arguments) arguments({{'v', nullptr}, {'a',"Aa"}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-v", "hello", "-a"}; // missing value + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testOk(strcmp(opts.argv0, "exe")==0, "%s", opts.argv0); + decltype (opts.arguments) arguments({{'v', nullptr}, {'?',nullptr}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-vvaTest", "hello"}; + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testOk(strcmp(opts.argv0, "exe")==0, "%s", opts.argv0); + decltype (opts.arguments) arguments({{'v', nullptr}, {'v', nullptr}, {'a', "Test"}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-vvQQ", "hello"}; + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testOk(strcmp(opts.argv0, "exe")==0, "%s", opts.argv0); + decltype (opts.arguments) arguments({{'v', nullptr}, {'v', nullptr}, {-1, nullptr}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "-v", "--", "-vvaTest", "hello"}; + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testOk(strcmp(opts.argv0, "exe")==0, "%s", opts.argv0); + decltype (opts.arguments) arguments({{'v', nullptr}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"-vvaTest", "hello"}); + testArrEq(opts.positional, positional); + } + + { + testDiag("case @%d", __LINE__); + const char* argv[] = {"exe", "--vvQQ", "hello"}; + pvxs::GetOpt opts(NELEMENTS(argv), const_cast(argv), "va:"); + testOk(strcmp(opts.argv0, "exe")==0, "%s", opts.argv0); + decltype (opts.arguments) arguments({{-1, "--vvQQ"}}); + testArrEq(opts.arguments, arguments); + decltype (opts.positional) positional({"hello"}); + testArrEq(opts.positional, positional); + } + + return testDone(); +} diff --git a/tools/testxput.cpp b/tools/testxput.cpp new file mode 100644 index 0000000..be818db --- /dev/null +++ b/tools/testxput.cpp @@ -0,0 +1,212 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include +#include + +#include + +#include +#include +#include + +#define PVXS_ENABLE_EXPERT_API + +#include +#include +#include +#include + +#define REALMAIN pvxput +#include "put.cpp" + +using namespace pvxs; + +namespace { +struct Redirect { + std::ostream& ref; + std::streambuf *prev; + Redirect(std::ostream& ref, std::ostream& target) + :ref(ref) + ,prev(ref.rdbuf(target.rdbuf())) + {} + ~Redirect() { + ref.rdbuf(prev); + } +}; + +struct Run { + std::string out, err; + int code; + + Run(std::initializer_list args) { + std::vector argv({"pvxput"}); + { + testCase cmdline; + cmdline<<"exec: pvxput"; + for(auto& arg : args) { + cmdline<<" '"<out = out.str(); + this->err = err.str(); + if(!this->out.empty()) + testShow()<<"stdout:\n"<out; + if(!this->err.empty()) + testShow()<<"stderr:\n"<err; + + } + + Run& exitWith(int expect) { + testOk(expect==code, "%d == %d", expect, code); + return *this; + } + Run& success() { + return exitWith(0); + } + + testCase _lineMatching(const std::string& inp, const std::string& expr) { + std::istringstream strm(inp); + std::string line; + testCase c; + while(std::getline(strm, line)) { + if(c.setPassMatch(expr, line)) + break; + } + return c; + } + + inline + testCase outMatch(const std::string& expr) { + return _lineMatching(out, expr); + } + inline + testCase errMatch(const std::string& expr) { + return _lineMatching(err, expr); + } +}; + +} // namespace + +MAIN(testxput) +{ + testPlan(28); + + auto pvI32(server::SharedPV::buildMailbox()); + pvI32.open(nt::NTScalar{TypeCode::Int32}.create() + .update("value", 5)); + + auto pvI32arr(server::SharedPV::buildMailbox()); + pvI32arr.open(nt::NTScalar{TypeCode::Int32A}.create()); + + auto pvS(server::SharedPV::buildMailbox()); + pvS.open(nt::NTScalar{TypeCode::String}.create() + .update("value", "foo")); + + auto pvE(server::SharedPV::buildMailbox()); + pvE.open(nt::NTEnum{}.create() + .update("value.index", 0) + .update("value.choices", shared_array({"one", "two"}))); + + // setup isolated server + auto srv(server::Config::isolated() + .build() + .addPV("testI32", pvI32) + .addPV("arrI32", pvI32arr) + .addPV("testS", pvS) + .addPV("testE", pvE) + .start()); + + // setup environment for pvxput() to find only our isolated server + { + client::Config::defs_t envs; + srv.clientConfig().updateDefs(envs); + for(const auto& pair : envs) { + testShow()<<" "<(), 6); + + Run({"testI32", "value=7"}).success(); + testEq(pvI32.fetch()["value"].as(), 7); + + Run({"testI32", R"({"value":8})"}).success(); + testEq(pvI32.fetch()["value"].as(), 8); + + Run({"arrI32", R"({"value":8})"}).exitWith(1); + + Run({"arrI32", R"([3,4,5])"}).success(); + testArrEq(pvI32arr.fetch()["value"].as>(), + shared_array({3,4,5})); + + Run({"arrI32", R"(value=[1,4])"}).success(); + testArrEq(pvI32arr.fetch()["value"].as>(), + shared_array({1,4})); + + Run({"arrI32", R"({"value":[5,6]})"}).success(); + testArrEq(pvI32arr.fetch()["value"].as>(), + shared_array({5, 6})); + + Run({"testS", "hello"}).success(); + testEq(pvS.fetch()["value"].as(), "hello"); + + Run({"testS", "value=world"}).success(); + testEq(pvS.fetch()["value"].as(), "world"); + + Run({"testS", R"({"value":"baz"})"}).success(); + testEq(pvS.fetch()["value"].as(), "baz"); + + + Run({"testE", R"({"value":{"index":1, "choices":["A","B","C"]}})"}).success(); + testEq(pvE.fetch()["value.index"].as(), 1); + + // update choices and assign from string with one op. A place where order matters... + Run({"testE", R"({"value":"d", "value.choices":["a","b","c","d"]})"}).exitWith(1); + + return testDone(); +}