Skip to content

Commit

Permalink
squash! test pvxput
Browse files Browse the repository at this point in the history
re-write argument parsing for testability
  • Loading branch information
mdavidsaver committed Jul 8, 2024
1 parent 471cf29 commit 3c92cdd
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 47 deletions.
6 changes: 6 additions & 0 deletions tools/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ USR_CPPFLAGS += -I$(TOP)/src

PROD_LIBS += pvxs Com

PROD_SRCS += cliutil.cpp

PROD += pvxvct
pvxvct_SRCS += pvxvct.cpp

Expand All @@ -39,6 +41,10 @@ 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
Expand Down
83 changes: 83 additions & 0 deletions tools/cliutil.cpp
Original file line number Diff line number Diff line change
@@ -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("<program name>")
{
if(argc>=1)
argv0 = argv[0];

bool allpos = false; // after "--", treat all remaining as positional
for(int i=1; i<argc; i++) {
const char * arg = argv[i];
if(!allpos && arg[0]=='-') {
arg++;

if(arg[1]=='-') {
if(arg[2]=='\0') { // "--"
allpos = true;
continue;
}
// "--..." not supported
arguments.emplace_back(-1, argv[i]);
return;
}
// process as short args

for(; *arg; arg++) {
for(auto s=spec; *s; s++) {
if(*s==*arg) { // match
if(s[1]==':') { // need arg value
if(arg[1]=='\0') { // "-a", "value"
if(i+1==argc) {
// oops. no value
arguments.emplace_back('?', nullptr);
return;
}
arguments.emplace_back(*arg, argv[i+1]);
i++;

} else {
// "-avalue"
arguments.emplace_back(*arg, &arg[1]);
}
goto nextarg;

} else { // flag
arguments.emplace_back(*s, nullptr);
// continue scanning for more flags. eg. "-vv"
goto nextchar;
}
} else {
if(s[1]==':')
s++;
}
}
// unrecognized
arguments.emplace_back(-1, nullptr);
return;
nextchar:
(void)0; // need a statement after label...
}
nextarg:
(void)0;

} else {
positional.push_back(arg);
}
success = true;
}
}

} // namespace pvxs
39 changes: 39 additions & 0 deletions tools/cliutil.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* 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.
*/

#ifndef CLIUTIL_H
#define CLIUTIL_H

#include <vector>
#include <string>
#include <map> // for std::pair

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) {}
};

bool operator==(const ArgVal& rhs, const ArgVal& lhs);

struct GetOpt {
GetOpt(int argc, char *argv[], const char *spec);

const char *argv0;
std::vector<std::string> positional;
std::vector<std::pair<char, ArgVal>> arguments;
bool success = false;
};

} // namespace pvxs

#endif // CLIUTIL_H
70 changes: 34 additions & 36 deletions tools/put.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
#include <atomic>

#include <epicsVersion.h>
#include <epicsGetopt.h>
#include <epicsThread.h>

#include <pvxs/client.h>
#include <pvxs/log.h>
#include <pvxs/json.h>
#include "utilpvt.h"
#include "evhelper.h"
#include "cliutil.h"

#ifndef REALMAIN
# define REALMAIN main
Expand Down Expand Up @@ -49,55 +49,53 @@ int REALMAIN(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<<pvxs::version_information;
return 0;
case 'v':
verbose = true;
break;
case 'd':
logger_level_set("pvxs.*", Level::Debug);
break;
case 'w':
timeout = parseTo<double>(optarg);
break;
case 'r':
request = optarg;
break;
default:
usage(argv[0]);
std::cerr<<"\nUnknown argument: "<<char(opt)<<std::endl;
return 1;
}
GetOpt opts(argc, argv, "hvVdw:r:");
for(auto& pair : opts.arguments) {
switch(pair.first) {
case 'h':
usage(opts.argv0);
return 0;
case 'V':
std::cout<<pvxs::version_information;
return 0;
case 'v':
verbose = true;
break;
case 'd':
logger_level_set("pvxs.*", Level::Debug);
break;
case 'w':
timeout = parseTo<double>(optarg);
break;
case 'r':
request = optarg;
break;
default:
usage(opts.argv0);
std::cerr<<"\nUnknown argument: "<<pair.first<<std::endl;
return 1;
}
}

if(optind==argc) {
usage(argv[0]);
std::cerr<<"\nExpected PV name\n";
if(opts.positional.size()<2) {
usage(opts.argv0);
std::cerr<<"\nExpected PV name and at least one value\n";
return 1;
}

std::string pvname(argv[optind++]);
const auto& pvname = opts.positional.front();
std::map<std::string, std::string> 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) {
Expand Down
110 changes: 110 additions & 0 deletions tools/testcliutil.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* 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 <vector>
#include <initializer_list>

#include <string.h>

#include <pvxs/unittest.h>
#include <pvxs/util.h>

#include <testMain.h>
#include <epicsUnitTest.h>
#include <dbDefs.h>

#include "cliutil.h"

namespace pvxs { namespace detail {

template<typename E>
struct test_print<std::vector<E>> {
template<class C>
static inline void op(C& strm, const std::vector<E>& v) {
bool first = true;
for(auto& e : v) {
if(first) {
first = false;
} else {
strm<<' ';
}
test_print<E>::op(strm, e);
}
}
};

template<>
struct test_print<std::pair<char, ArgVal>> {
template<class C>
static inline void op(C& strm, const std::pair<char, ArgVal>& v) {
strm<<'-'<<v.first;
if(v.second.defined)
strm<<" "<<pvxs::escape(v.second.value);
}
};

}} // namespace::detail

MAIN(testcliutil) {
testPlan(15);

{
testDiag("case @%d", __LINE__);
const char* argv[] = {"exe", "-v", "-a", "Aa", "hello"};
pvxs::GetOpt opts(NELEMENTS(argv), const_cast<char**>(argv), "a:v");
testEq(opts.argv0, "exe");
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<char**>(argv), "va:");
testEq(opts.argv0, "exe");
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<char**>(argv), "va:");
testEq(opts.argv0, "exe");
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<char**>(argv), "va:");
testEq(opts.argv0, "exe");
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<char**>(argv), "va:");
testEq(opts.argv0, "exe");
decltype (opts.arguments) arguments({{'v', nullptr}, {'v', nullptr}, {'a', "Test"}});
testArrEq(opts.arguments, arguments);
decltype (opts.positional) positional({"hello"});
testArrEq(opts.positional, positional);
}

return testDone();
}
Loading

0 comments on commit 3c92cdd

Please sign in to comment.