From f06f25532ca06b7edb948131f45f8efea2268589 Mon Sep 17 00:00:00 2001 From: James Turner Date: Tue, 19 Jul 2011 12:55:55 +0100 Subject: [PATCH] Tiny HTTP client layer on top of NetChat - and CTest support for some SimGear tests. --- CMakeLists.txt | 6 + simgear/io/CMakeLists.txt | 13 +- simgear/io/HTTPClient.cxx | 240 +++++++++++++++++++++++++++++ simgear/io/HTTPClient.hxx | 51 ++++++ simgear/io/HTTPRequest.cxx | 133 ++++++++++++++++ simgear/io/HTTPRequest.hxx | 69 +++++++++ simgear/io/sg_netChat.cxx | 34 +++- simgear/io/sg_netChat.hxx | 14 +- simgear/io/test_HTTP.cxx | 274 +++++++++++++++++++++++++++++++++ simgear/misc/CMakeLists.txt | 8 + simgear/misc/strutils.cxx | 36 +++++ simgear/misc/strutils.hxx | 8 + simgear/misc/strutils_test.cxx | 58 +++++++ simgear/props/CMakeLists.txt | 8 + simgear/timing/timestamp.cxx | 7 + simgear/timing/timestamp.hxx | 4 + 16 files changed, 953 insertions(+), 10 deletions(-) create mode 100644 simgear/io/HTTPClient.cxx create mode 100644 simgear/io/HTTPClient.hxx create mode 100644 simgear/io/HTTPRequest.cxx create mode 100644 simgear/io/HTTPRequest.hxx create mode 100644 simgear/io/test_HTTP.cxx create mode 100644 simgear/misc/strutils_test.cxx diff --git a/CMakeLists.txt b/CMakeLists.txt index 9374a3ee..73bfdb51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,11 @@ configure_file ( "${PROJECT_BINARY_DIR}/simgear/simgear_config.h" ) +# enable CTest / make test target + +include (Dart) +enable_testing() + install (FILES ${PROJECT_BINARY_DIR}/simgear/simgear_config.h DESTINATION include/simgear/) add_subdirectory(simgear) @@ -156,3 +161,4 @@ ADD_CUSTOM_TARGET(uninstall "${CMAKE_COMMAND}" -P "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake") + diff --git a/simgear/io/CMakeLists.txt b/simgear/io/CMakeLists.txt index e00b6945..1d4600f6 100644 --- a/simgear/io/CMakeLists.txt +++ b/simgear/io/CMakeLists.txt @@ -14,6 +14,8 @@ set(HEADERS sg_serial.hxx sg_socket.hxx sg_socket_udp.hxx + HTTPClient.hxx + HTTPRequest.hxx ) set(SOURCES @@ -28,6 +30,15 @@ set(SOURCES sg_serial.cxx sg_socket.cxx sg_socket_udp.cxx + HTTPClient.cxx + HTTPRequest.cxx ) -simgear_component(io io "${SOURCES}" "${HEADERS}") \ No newline at end of file +simgear_component(io io "${SOURCES}" "${HEADERS}") + +add_executable(test_sock socktest.cxx) +target_link_libraries(test_sock sgio sgstructure sgdebug) + +add_executable(test_http test_HTTP.cxx) +target_link_libraries(test_http sgio sgstructure sgtiming sgmisc sgdebug) + diff --git a/simgear/io/HTTPClient.cxx b/simgear/io/HTTPClient.cxx new file mode 100644 index 00000000..fb8b256b --- /dev/null +++ b/simgear/io/HTTPClient.cxx @@ -0,0 +1,240 @@ +#include "HTTPClient.hxx" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using std::string; +using std::stringstream; +using std::vector; + +#include + +using std::cout; +using std::cerr; +using std::endl; + +namespace simgear +{ + +namespace HTTP +{ + + +class Connection : public NetChat +{ +public: + Connection(Client* pr) : + client(pr), + state(STATE_IDLE) + { + setTerminator("\r\n"); + } + + void connectToHost(const string& host) + { + open(); + + int colonPos = host.find(':'); + if (colonPos > 0) { + string h = host.substr(0, colonPos); + int port = strutils::to_int(host.substr(colonPos + 1)); + connect(h.c_str(), port); + } else { + connect(host.c_str(), 80 /* default port */); + } + } + + void queueRequest(const Request_ptr& r) + { + if (!activeRequest) { + startRequest(r); + } else { + queuedRequests.push_back(r); + } + } + + void startRequest(const Request_ptr& r) + { + activeRequest = r; + state = STATE_IDLE; + bodyTransferSize = 0; + + stringstream headerData; + string path = r->path(); + if (!client->proxyHost().empty()) { + path = "http://" + r->host() + path; + } + + int requestTime; + headerData << r->method() << " " << path << " HTTP/1.1 " << client->userAgent() << "\r\n"; + headerData << "Host: " << r->host() << "\r\n"; + headerData << "X-Time: " << requestTime << "\r\n"; + + if (!client->proxyAuth().empty()) { + headerData << "Proxy-Authorization: " << client->proxyAuth() << "\r\n"; + } + + BOOST_FOREACH(string h, r->requestHeaders()) { + headerData << h << ": " << r->header(h) << "\r\n"; + } + + headerData << "\r\n"; // final CRLF to terminate the headers + + // TODO - add request body support for PUT, etc operations + + push(headerData.str().c_str()); + cout << "sent request" << endl; + } + + virtual void collectIncomingData(const char* s, int n) + { + if (state == STATE_GETTING_BODY) { + activeRequest->gotBodyData(s, n); + } else { + buffer += string(s, n); + } + } + + virtual void foundTerminator(void) + { + switch (state) { + case STATE_IDLE: + activeRequest->responseStart(buffer); + state = STATE_GETTING_HEADERS; + buffer.clear(); + break; + + case STATE_GETTING_HEADERS: + processHeader(); + buffer.clear(); + break; + + case STATE_GETTING_BODY: + responseComplete(); + state = STATE_IDLE; + setTerminator("\r\n"); + + if (!queuedRequests.empty()) { + Request_ptr next = queuedRequests.front(); + queuedRequests.pop_front(); + startRequest(next); + } + + break; + } + } + +private: + void processHeader() + { + string h = strutils::simplify(buffer); + if (h.empty()) { // blank line terminates headers + headersComplete(); + + if (bodyTransferSize > 0) { + state = STATE_GETTING_BODY; + cout << "getting body:" << bodyTransferSize << endl; + setByteCount(bodyTransferSize); + } else { + responseComplete(); + state = STATE_IDLE; // no response body, we're done + } + return; + } + + int colonPos = buffer.find(':'); + if (colonPos < 0) { + SG_LOG(SG_IO, SG_WARN, "malformed HTTP response header:" << h); + return; + } + + string key = strutils::simplify(buffer.substr(0, colonPos)); + string lkey = boost::to_lower_copy(key); + string value = strutils::strip(buffer.substr(colonPos + 1)); + + if (lkey == "content-length" && (bodyTransferSize <= 0)) { + bodyTransferSize = strutils::to_int(value); + } else if (lkey == "transfer-length") { + bodyTransferSize = strutils::to_int(value); + } + + activeRequest->responseHeader(lkey, value); + } + + void headersComplete() + { + activeRequest->responseHeadersComplete(); + } + + void responseComplete() + { + activeRequest->responseComplete(); + client->requestFinished(this); + activeRequest = NULL; + } + + enum ConnectionState { + STATE_IDLE = 0, + STATE_GETTING_HEADERS, + STATE_GETTING_BODY + }; + + Client* client; + Request_ptr activeRequest; + ConnectionState state; + std::string buffer; + int bodyTransferSize; + + std::list queuedRequests; +}; + +Client::Client() +{ + setUserAgent("SimGear-" SG_STRINGIZE(SIMGEAR_VERSION)); +} + +void Client::makeRequest(const Request_ptr& r) +{ + string host = r->host(); + if (!_proxy.empty()) { + host = _proxy; + } + + if (_connections.find(host) == _connections.end()) { + Connection* con = new Connection(this); + con->connectToHost(host); + _connections[host] = con; + } + + _connections[host]->queueRequest(r); +} + +void Client::requestFinished(Connection* con) +{ + +} + +void Client::setUserAgent(const string& ua) +{ + _userAgent = ua; +} + +void Client::setProxy(const string& proxy, const string& auth) +{ + _proxy = proxy; + _proxyAuth = auth; +} + +} // of namespace HTTP + +} // of namespace simgear diff --git a/simgear/io/HTTPClient.hxx b/simgear/io/HTTPClient.hxx new file mode 100644 index 00000000..0e04cdc9 --- /dev/null +++ b/simgear/io/HTTPClient.hxx @@ -0,0 +1,51 @@ +#ifndef SG_HTTP_CLIENT_HXX +#define SG_HTTP_CLIENT_HXX + +#include + +#include + +namespace simgear +{ + +namespace HTTP +{ + +class Connection; + +class Client +{ +public: + Client(); + + void makeRequest(const Request_ptr& r); + + void setUserAgent(const std::string& ua); + void setProxy(const std::string& proxy, const std::string& auth = ""); + + const std::string& userAgent() const + { return _userAgent; } + + const std::string& proxyHost() const + { return _proxy; } + + const std::string& proxyAuth() const + { return _proxyAuth; } +private: + void requestFinished(Connection* con); + + friend class Connection; + + std::string _userAgent; + std::string _proxy; + std::string _proxyAuth; + +// connections by host + std::map _connections; +}; + +} // of namespace HTTP + +} // of namespace simgear + +#endif // of SG_HTTP_CLIENT_HXX diff --git a/simgear/io/HTTPRequest.cxx b/simgear/io/HTTPRequest.cxx new file mode 100644 index 00000000..e38f7aae --- /dev/null +++ b/simgear/io/HTTPRequest.cxx @@ -0,0 +1,133 @@ +#include "HTTPRequest.hxx" + +#include +#include +#include + +using std::string; +using std::map; + +#include + +using std::cout; +using std::cerr; +using std::endl; + +namespace simgear +{ + +namespace HTTP +{ + +Request::Request(const string& url, const string method) : + _method(method), + _url(url) +{ + +} + +Request::~Request() +{ + +} + +string_list Request::requestHeaders() const +{ + string_list r; + return r; +} + +string Request::header(const std::string& name) const +{ + return string(); +} + +void Request::responseStart(const string& r) +{ + const int maxSplit = 2; // HTTP/1.1 nnn status code + string_list parts = strutils::split(r, NULL, maxSplit); + _responseStatus = strutils::to_int(parts[1]); + _responseReason = parts[2]; +} + +void Request::responseHeader(const string& key, const string& value) +{ + _responseHeaders[key] = value; +} + +void Request::responseHeadersComplete() +{ + // no op +} + +void Request::gotBodyData(const char* s, int n) +{ + +} + +void Request::responseComplete() +{ + +} + +string Request::scheme() const +{ + int firstColon = url().find(":"); + if (firstColon > 0) { + return url().substr(0, firstColon); + } + + return ""; // couldn't parse scheme +} + +string Request::path() const +{ + string u(url()); + int schemeEnd = u.find("://"); + if (schemeEnd < 0) { + return ""; // couldn't parse scheme + } + + int hostEnd = u.find('/', schemeEnd + 3); + if (hostEnd < 0) { + return ""; // couldn't parse host + } + + int query = u.find('?', hostEnd + 1); + if (query < 0) { + // all remainder of URL is path + return u.substr(hostEnd); + } + + return u.substr(hostEnd, query - hostEnd); +} + +string Request::host() const +{ + string u(url()); + int schemeEnd = u.find("://"); + if (schemeEnd < 0) { + return ""; // couldn't parse scheme + } + + int hostEnd = u.find('/', schemeEnd + 3); + if (hostEnd < 0) { // all remainder of URL is host + return u.substr(schemeEnd + 3); + } + + return u.substr(schemeEnd + 3, hostEnd - (schemeEnd + 3)); +} + +int Request::contentLength() const +{ + HeaderDict::const_iterator it = _responseHeaders.find("content-length"); + if (it == _responseHeaders.end()) { + return 0; + } + + return strutils::to_int(it->second); +} + +} // of namespace HTTP + +} // of namespace simgear diff --git a/simgear/io/HTTPRequest.hxx b/simgear/io/HTTPRequest.hxx new file mode 100644 index 00000000..4afb1c75 --- /dev/null +++ b/simgear/io/HTTPRequest.hxx @@ -0,0 +1,69 @@ +#ifndef SG_HTTP_REQUEST_HXX +#define SG_HTTP_REQUEST_HXX + +#include + +#include +#include +#include + +namespace simgear +{ + +namespace HTTP +{ + +class Request : public SGReferenced +{ +public: + virtual ~Request(); + + virtual std::string method() const + { return _method; } + virtual std::string url() const + { return _url; } + + virtual std::string scheme() const; + virtual std::string path() const; + virtual std::string host() const; // host, including port + + virtual string_list requestHeaders() const; + virtual std::string header(const std::string& name) const; + + virtual int responseCode() const + { return _responseStatus; } + + virtual std::string resposeReason() const + { return _responseReason; } + + virtual int contentLength() const; +protected: + friend class Connection; + + Request(const std::string& url, const std::string method = "get"); + + virtual void responseStart(const std::string& r); + virtual void responseHeader(const std::string& key, const std::string& value); + virtual void responseHeadersComplete(); + virtual void responseComplete(); + + virtual void gotBodyData(const char* s, int n); +private: + + std::string _method; + std::string _url; + int _responseStatus; + std::string _responseReason; + + typedef std::map HeaderDict; + HeaderDict _responseHeaders; +}; + +typedef SGSharedPtr Request_ptr; + +} // of namespace HTTP + +} // of namespace simgear + +#endif // of SG_HTTP_REQUEST_HXX + diff --git a/simgear/io/sg_netChat.cxx b/simgear/io/sg_netChat.cxx index fcced48e..597bb47e 100644 --- a/simgear/io/sg_netChat.cxx +++ b/simgear/io/sg_netChat.cxx @@ -26,14 +26,15 @@ #include #include // for strdup - + namespace simgear { void NetChat::setTerminator (const char* t) { - if (terminator) delete[] terminator; + if (terminator) free(terminator); terminator = strdup(t); + bytesToCollect = -1; } const char* @@ -42,6 +43,15 @@ NetChat::getTerminator (void) return terminator; } + +void +NetChat::setByteCount(int count) +{ + if (terminator) free(terminator); + terminator = NULL; + bytesToCollect = count; +} + // return the size of the largest prefix of needle at the end // of haystack @@ -89,12 +99,22 @@ NetChat::handleBufferRead (NetBuffer& in_buffer) // necessary because we might read several data+terminator combos // with a single recv(). - while (in_buffer.getLength()) { - + while (in_buffer.getLength()) { // special case where we're not using a terminator - if (terminator == 0 || *terminator == 0) { - collectIncomingData (in_buffer.getData(),in_buffer.getLength()); - in_buffer.remove (); + if (terminator == 0 || *terminator == 0) { + if ( bytesToCollect > 0) { + const int toRead = std::min(in_buffer.getLength(), bytesToCollect); + collectIncomingData(in_buffer.getData(), toRead); + in_buffer.remove(0, toRead); + bytesToCollect -= toRead; + if (bytesToCollect == 0) { // read all requested bytes + foundTerminator(); + } + } else { // read the whole lot + collectIncomingData (in_buffer.getData(),in_buffer.getLength()); + in_buffer.remove (); + } + return; } diff --git a/simgear/io/sg_netChat.hxx b/simgear/io/sg_netChat.hxx index e9ab2e03..8c346728 100644 --- a/simgear/io/sg_netChat.hxx +++ b/simgear/io/sg_netChat.hxx @@ -61,6 +61,7 @@ #ifndef SG_NET_CHAT_H #define SG_NET_CHAT_H +#include #include namespace simgear @@ -69,16 +70,25 @@ namespace simgear class NetChat : public NetBufferChannel { char* terminator; - + int bytesToCollect; virtual void handleBufferRead (NetBuffer& buffer) ; public: - NetChat () : terminator (0) {} + NetChat () : + terminator (NULL), + bytesToCollect(-1) + {} void setTerminator (const char* t); const char* getTerminator (void); + /** + * set byte count to collect - 'foundTerminator' will be called once + * this many bytes have been collected + */ + void setByteCount(int bytes); + bool push (const char* s); virtual void collectIncomingData (const char* s, int n) {} diff --git a/simgear/io/test_HTTP.cxx b/simgear/io/test_HTTP.cxx new file mode 100644 index 00000000..3d536498 --- /dev/null +++ b/simgear/io/test_HTTP.cxx @@ -0,0 +1,274 @@ +#include + +#include +#include +#include + +#include + +#include "HTTPClient.hxx" +#include "HTTPRequest.hxx" + +#include +#include +#include + +using std::cout; +using std::cerr; +using std::endl; +using std::string; +using std::stringstream; + +using namespace simgear; + +const char* BODY1 = "The quick brown fox jumps over a lazy dog."; + +const int body2Size = 8 * 1024; +char body2[body2Size]; + +#define COMPARE(a, b) \ + if ((a) != (b)) { \ + cerr << "failed:" << #a << " != " << #b << endl; \ + cerr << "\tgot:" << a << endl; \ + exit(1); \ + } + +#define VERIFY(a) \ + if (!(a)) { \ + cerr << "failed:" << #a << endl; \ + exit(1); \ + } + +class TestRequest : public HTTP::Request +{ +public: + bool complete; + string bodyData; + + TestRequest(const std::string& url) : + HTTP::Request(url), + complete(false) + { + + } + +protected: + virtual void responseHeadersComplete() + { + } + + virtual void responseComplete() + { + complete = true; + } + + virtual void gotBodyData(const char* s, int n) + { + bodyData += string(s, n); + } +}; + +class TestServerChannel : public NetChat +{ +public: + enum State + { + STATE_IDLE = 0, + STATE_HEADERS, + STATE_REQUEST_BODY + }; + + TestServerChannel() + { + state = STATE_IDLE; + setTerminator("\r\n"); + } + + virtual void collectIncomingData(const char* s, int n) + { + buffer += string(s, n); + } + + virtual void foundTerminator(void) + { + if (state == STATE_IDLE) { + state = STATE_HEADERS; + string_list line = strutils::split(buffer, NULL, 3); + if (line.size() < 4) { + cerr << "malformed request:" << buffer << endl; + exit(-1); + } + + method = line[0]; + path = line[1]; + httpVersion = line[2]; + userAgent = line[3]; + requestHeaders.clear(); + buffer.clear(); + } else if (state == STATE_HEADERS) { + string s = strutils::simplify(buffer); + if (s.empty()) { + buffer.clear(); + receivedRequestHeaders(); + return; + } + + int colonPos = buffer.find(':'); + if (colonPos < 0) { + cerr << "malformed HTTP response header:" << buffer << endl; + buffer.clear(); + return; + } + + string key = strutils::simplify(buffer.substr(0, colonPos)); + string lkey = boost::to_lower_copy(key); + string value = strutils::strip(buffer.substr(colonPos + 1)); + requestHeaders[lkey] = value; + buffer.clear(); + } else if (state == STATE_REQUEST_BODY) { + + } + } + + void receivedRequestHeaders() + { + state = STATE_IDLE; + if (path == "/test1") { + string contentStr(BODY1); + stringstream d; + d << "HTTP1.1 " << 200 << " " << reasonForCode(200) << "\r\n"; + d << "Content-Length:" << contentStr.size() << "\r\n"; + d << "\r\n"; // final CRLF to terminate the headers + d << contentStr; + push(d.str().c_str()); + } else if (path == "/test2") { + stringstream d; + d << "HTTP1.1 " << 200 << " " << reasonForCode(200) << "\r\n"; + d << "Content-Length:" << body2Size << "\r\n"; + d << "\r\n"; // final CRLF to terminate the headers + push(d.str().c_str()); + bufferSend(body2, body2Size); + cout << "sent body2" << endl; + } else { + sendErrorResponse(404); + } + } + + void sendErrorResponse(int code) + { + cerr << "sending error " << code << " for " << path << endl; + stringstream headerData; + headerData << "HTTP1.1 " << code << " " << reasonForCode(code) << "\r\n"; + headerData << "\r\n"; // final CRLF to terminate the headers + push(headerData.str().c_str()); + } + + string reasonForCode(int code) + { + switch (code) { + case 200: return "OK"; + case 404: return "not found"; + default: return "unknown code"; + } + } + + State state; + string buffer; + string method; + string path; + string httpVersion; + string userAgent; + std::map requestHeaders; +}; + +class TestServer : public NetChannel +{ +public: + TestServer() + { + open(); + bind(NULL, 2000); // localhost, any port + listen(5); + } + + virtual ~TestServer() + { + } + + virtual bool writable (void) { return false ; } + + virtual void handleAccept (void) + { + simgear::IPAddress addr ; + int handle = accept ( &addr ) ; + + TestServerChannel* chan = new TestServerChannel(); + chan->setHandle(handle); + } +}; + +void waitForComplete(TestRequest* tr) +{ + SGTimeStamp start(SGTimeStamp::now()); + while (start.elapsedMSec() < 1000) { + NetChannel::poll(10); + if (tr->complete) { + return; + } + } + + cerr << "timed out" << endl; +} + +int main(int argc, char* argv[]) +{ + TestServer s; + + HTTP::Client cl; + +// test URL parsing + TestRequest* tr1 = new TestRequest("http://localhost:2000/test1?foo=bar"); + COMPARE(tr1->scheme(), "http"); + COMPARE(tr1->host(), "localhost:2000"); + COMPARE(tr1->path(), "/test1"); + +// basic get request + { + TestRequest* tr = new TestRequest("http://localhost:2000/test1"); + HTTP::Request_ptr own(tr); + cl.makeRequest(tr); + + waitForComplete(tr); + COMPARE(tr->responseCode(), 200); + COMPARE(tr->contentLength(), strlen(BODY1)); + COMPARE(tr->bodyData, string(BODY1)); + } + +// larger get request + for (int i=0; i> 2); + } + + { + TestRequest* tr = new TestRequest("http://localhost:2000/test2"); + HTTP::Request_ptr own(tr); + cl.makeRequest(tr); + waitForComplete(tr); + COMPARE(tr->responseCode(), 200); + COMPARE(tr->contentLength(), body2Size); + COMPARE(tr->bodyData, string(body2, body2Size)); + } + +// test 404 + { + TestRequest* tr = new TestRequest("http://localhost:2000/not-found"); + HTTP::Request_ptr own(tr); + cl.makeRequest(tr); + waitForComplete(tr); + COMPARE(tr->responseCode(), 404); + COMPARE(tr->contentLength(), 0); + } + + cout << "all tests passed ok" << endl; + return EXIT_SUCCESS; +} diff --git a/simgear/misc/CMakeLists.txt b/simgear/misc/CMakeLists.txt index f6c66277..6b21c7ca 100644 --- a/simgear/misc/CMakeLists.txt +++ b/simgear/misc/CMakeLists.txt @@ -33,3 +33,11 @@ set(SOURCES ) simgear_component(misc misc "${SOURCES}" "${HEADERS}") + +add_executable(test_tabbed_values tabbed_values_test.cxx) +add_test(tabbed_values ${EXECUTABLE_OUTPUT_PATH}/test_tabbed_values) +target_link_libraries(test_tabbed_values sgmisc) + +add_executable(test_strings strutils_test.cxx ) +add_test(test_strings ${EXECUTABLE_OUTPUT_PATH}/test_strings) +target_link_libraries(test_strings sgmisc) diff --git a/simgear/misc/strutils.cxx b/simgear/misc/strutils.cxx index a81adc5c..d4ff8393 100644 --- a/simgear/misc/strutils.cxx +++ b/simgear/misc/strutils.cxx @@ -213,5 +213,41 @@ namespace simgear { return (n != string::npos) && (n == s.length() - substr.length()); } + string simplify(const string& s) + { + string result; // reserve size of 's'? + string::const_iterator it = s.begin(), + end = s.end(); + + // advance to first non-space char - simplifes logic in main loop, + // since we can always prepend a single space when we see a + // space -> non-space transition + for (; (it != end) && isspace(*it); ++it) { /* nothing */ } + + bool lastWasSpace = false; + for (; it != end; ++it) { + char c = *it; + if (isspace(c)) { + lastWasSpace = true; + continue; + } + + if (lastWasSpace) { + result.push_back(' '); + } + + lastWasSpace = false; + result.push_back(c); + } + + return result; + } + + int to_int(const std::string& s) + { + return atoi(s.c_str()); + } + } // end namespace strutils + } // end namespace simgear diff --git a/simgear/misc/strutils.hxx b/simgear/misc/strutils.hxx index 935e3d4a..eeb1bfaa 100644 --- a/simgear/misc/strutils.hxx +++ b/simgear/misc/strutils.hxx @@ -116,6 +116,14 @@ namespace simgear { */ bool ends_with( const std::string & s, const std::string & substr ); + /** + * Strip all leading/trailing whitespace, and transform all interal + * whitespace into a single ' ' character - i.e newlines/carriage returns/ + * tabs/multiple spaces will be condensed. + */ + std::string simplify(const std::string& s); + + int to_int(const std::string& s); } // end namespace strutils } // end namespace simgear diff --git a/simgear/misc/strutils_test.cxx b/simgear/misc/strutils_test.cxx new file mode 100644 index 00000000..f6e08e08 --- /dev/null +++ b/simgear/misc/strutils_test.cxx @@ -0,0 +1,58 @@ +//////////////////////////////////////////////////////////////////////// +// Test harness. +//////////////////////////////////////////////////////////////////////// + +#include + +#include +#include "strutils.hxx" + +using std::cout; +using std::cerr; +using std::endl; + +using namespace simgear::strutils; + +#define COMPARE(a, b) \ + if ((a) != (b)) { \ + cerr << "failed:" << #a << " != " << #b << endl; \ + exit(1); \ + } + +#define VERIFY(a) \ + if (!(a)) { \ + cerr << "failed:" << #a << endl; \ + exit(1); \ + } + +int main (int ac, char ** av) +{ + std::string a("abcd"); + COMPARE(strip(a), a); + COMPARE(strip(" a "), "a"); + COMPARE(lstrip(" a "), "a "); + COMPARE(rstrip("\ta "), "\ta"); + // check internal spacing is preserved + COMPARE(strip("\t \na \t b\r \n "), "a \t b"); + + + VERIFY(starts_with("banana", "ban")); + VERIFY(!starts_with("abanana", "ban")); + VERIFY(starts_with("banana", "banana")); // pass - string starts with itself + VERIFY(!starts_with("ban", "banana")); // fail - original string is prefix of + + VERIFY(ends_with("banana", "ana")); + VERIFY(ends_with("foo.text", ".text")); + VERIFY(!ends_with("foo.text", ".html")); + + COMPARE(simplify("\ta\t b \nc\n\r \r\n"), "a b c"); + COMPARE(simplify("The quick - brown dog!"), "The quick - brown dog!"); + COMPARE(simplify("\r\n \r\n \t \r"), ""); + + COMPARE(to_int("999"), 999); + COMPARE(to_int("0000000"), 0); + COMPARE(to_int("-10000"), -10000); + + cout << "all tests passed successfully!" << endl; + return 0; +} diff --git a/simgear/props/CMakeLists.txt b/simgear/props/CMakeLists.txt index f57b9253..f347df45 100644 --- a/simgear/props/CMakeLists.txt +++ b/simgear/props/CMakeLists.txt @@ -20,3 +20,11 @@ set(SOURCES ) simgear_component(props props "${SOURCES}" "${HEADERS}") + +add_executable(test_props props_test.cxx) +target_link_libraries(test_props sgprops sgxml sgstructure sgmisc sgdebug) +add_test(test_props ${EXECUTABLE_OUTPUT_PATH}/test_props) + +add_executable(test_propertyObject propertyObject_test.cxx) +target_link_libraries(test_propertyObject sgprops sgstructure sgdebug) +add_test(test_propertyObject ${EXECUTABLE_OUTPUT_PATH}/test_propertyObject) diff --git a/simgear/timing/timestamp.cxx b/simgear/timing/timestamp.cxx index bfed6644..83eff2fc 100644 --- a/simgear/timing/timestamp.cxx +++ b/simgear/timing/timestamp.cxx @@ -104,3 +104,10 @@ void SGTimeStamp::stamp() { #endif } +int SGTimeStamp::elapsedMSec() const +{ + SGTimeStamp now; + now.stamp(); + + return static_cast((now - *this).toMSecs()); +} diff --git a/simgear/timing/timestamp.hxx b/simgear/timing/timestamp.hxx index e6aeb0a2..f195dc37 100644 --- a/simgear/timing/timestamp.hxx +++ b/simgear/timing/timestamp.hxx @@ -195,6 +195,10 @@ public: static SGTimeStamp now() { SGTimeStamp ts; ts.stamp(); return ts; } + /** + * elapsed time since the stamp was taken, in msec + */ + int elapsedMSec() const; private: SGTimeStamp(sec_type sec, nsec_type nsec) { setTime(sec, nsec); } -- 2.39.5