changeset 158:0876b5e67afe

Improve error handling and extend tests for it
author Chris Cannam <cannam@all-day-breakfast.com>
date Fri, 20 Jan 2017 22:24:44 +0000
parents 5699fca64251
children 2ada256e47e1
files vamp-capnp/VampnProto.h vamp-json/VampJson.h vamp-server/convert.cpp vamp-server/simple-server.cpp vamp-server/test.sh
diffstat 5 files changed, 110 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- a/vamp-capnp/VampnProto.h	Fri Jan 20 22:24:11 2017 +0000
+++ b/vamp-capnp/VampnProto.h	Fri Jan 20 22:24:44 2017 +0000
@@ -874,7 +874,13 @@
         }
 
         e.setCode(0);
-        e.setMessage(std::string("error in ") + type + " request: " + errorText);
+
+        if (responseType == RRType::NotValid) {
+            e.setMessage(errorText);
+        } else {
+            e.setMessage
+                (std::string("error in ") + type + " request: " + errorText);
+        }
     }
 
     static void
--- a/vamp-json/VampJson.h	Fri Jan 20 22:24:11 2017 +0000
+++ b/vamp-json/VampJson.h	Fri Jan 20 22:24:44 2017 +0000
@@ -1314,8 +1314,13 @@
 
         json11::Json::object eo;
         eo["code"] = 0;
-        eo["message"] = 
-            std::string("error in ") + type + " request: " + errorText;
+
+        if (responseType == RRType::NotValid) {
+            eo["message"] = errorText;
+        } else {
+            eo["message"] = 
+                std::string("error in ") + type + " request: " + errorText;
+        }
 
         jo["method"] = type;
         jo["error"] = eo;
--- a/vamp-server/convert.cpp	Fri Jan 20 22:24:11 2017 +0000
+++ b/vamp-server/convert.cpp	Fri Jan 20 22:24:44 2017 +0000
@@ -187,7 +187,7 @@
 }
 
 RequestOrResponse
-readRequestJson(string &err)
+readRequestJson(string &err, bool &eof)
 {
     RequestOrResponse rr;
     rr.direction = RequestOrResponse::Request;
@@ -195,7 +195,7 @@
     string input;
     if (!getline(cin, input)) {
         // the EOF case, not actually an error
-        rr.type = RRType::NotValid;
+        eof = true;
         return rr;
     }
     
@@ -272,7 +272,7 @@
 }
 
 RequestOrResponse
-readResponseJson(string &err)
+readResponseJson(string &err, bool &eof)
 {
     RequestOrResponse rr;
     rr.direction = RequestOrResponse::Response;
@@ -280,7 +280,7 @@
     string input;
     if (!getline(cin, input)) {
         // the EOF case, not actually an error
-        rr.type = RRType::NotValid;
+        eof = true;
         return rr;
     }
 
@@ -361,6 +361,7 @@
                 (rr.finishResponse, mapper, serialisation, id);
             break;
         case RRType::NotValid:
+            j = VampJson::fromError(rr.errorText, rr.type, id);
             break;
         }
     }
@@ -472,8 +473,6 @@
         VampnProto::readRpcResponse_Finish(rr.finishResponse, reader, mapper);
         break;
     case RRType::NotValid:
-        // error
-        rr.success = false;
         VampnProto::readRpcResponse_Error(errorCode, rr.errorText, reader);
         break;
     }
@@ -513,6 +512,7 @@
             VampnProto::buildRpcResponse_Finish(builder, rr.finishResponse, mapper);
             break;
         case RRType::NotValid:
+            VampnProto::buildRpcResponse_Error(builder, rr.errorText, rr.type);
             break;
         }
     }
@@ -521,22 +521,23 @@
 }
 
 RequestOrResponse
-readInputJson(RequestOrResponse::Direction direction, string &err)
+readInputJson(RequestOrResponse::Direction direction, string &err, bool &eof)
 {
     if (direction == RequestOrResponse::Request) {
-        return readRequestJson(err);
+        return readRequestJson(err, eof);
     } else {
-        return readResponseJson(err);
+        return readResponseJson(err, eof);
     }
 }
 
 RequestOrResponse
-readInputCapnp(RequestOrResponse::Direction direction)
+readInputCapnp(RequestOrResponse::Direction direction, bool &eof)
 {
     static kj::FdInputStream stream(0); // stdin
     static kj::BufferedInputStreamWrapper buffered(stream);
 
     if (buffered.tryGetReadBuffer() == nullptr) {
+        eof = true;
         return {};
     }
     
@@ -548,15 +549,17 @@
 }
 
 RequestOrResponse
-readInput(string format, RequestOrResponse::Direction direction)
+readInput(string format, RequestOrResponse::Direction direction, bool &eof)
 {
+    eof = false;
+    
     if (format == "json") {
         string err;
-        auto result = readInputJson(direction, err);
+        auto result = readInputJson(direction, err, eof);
         if (err != "") throw runtime_error(err);
         else return result;
     } else if (format == "capnp") {
-        return readInputCapnp(direction);
+        return readInputCapnp(direction, eof);
     } else {
         throw runtime_error("unknown input format \"" + format + "\"");
     }
@@ -649,13 +652,12 @@
 
         try {
 
-            RequestOrResponse rr = readInput(informat, direction);
-
-            // NotValid without an exception indicates EOF:
-            if (rr.type == RRType::NotValid) break;
+            bool eof = false;
+            RequestOrResponse rr = readInput(informat, direction, eof);
+            if (eof) break;
 
             writeOutput(outformat, rr);
-            
+
         } catch (std::exception &e) {
 
             cerr << "Error: " << e.what() << endl;
--- a/vamp-server/simple-server.cpp	Fri Jan 20 22:24:11 2017 +0000
+++ b/vamp-server/simple-server.cpp	Fri Jan 20 22:24:44 2017 +0000
@@ -245,7 +245,7 @@
 }
 
 RequestOrResponse
-readRequestJson(string &err)
+readRequestJson(string &err, bool &eof)
 {
     RequestOrResponse rr;
     rr.direction = RequestOrResponse::Request;
@@ -253,7 +253,7 @@
     string input;
     if (!getline(cin, input)) {
         // the EOF case, not actually an error
-        rr.type = RRType::NotValid;
+        eof = true;
         return rr;
     }
     
@@ -339,14 +339,15 @@
 }
 
 void
-writeExceptionJson(const exception &e, RRType type)
+writeExceptionJson(const exception &e, RRType type, RequestOrResponse::RpcId id)
 {
-    Json j = VampJson::fromError(e.what(), type, Json());
+    Json jid = writeJsonId(id);
+    Json j = VampJson::fromError(e.what(), type, jid);
     cout << j.dump() << endl;
 }
 
 RequestOrResponse
-readRequestCapnp()
+readRequestCapnp(bool &eof)
 {
     RequestOrResponse rr;
     rr.direction = RequestOrResponse::Request;
@@ -355,7 +356,7 @@
     static kj::BufferedInputStreamWrapper buffered(stream);
 
     if (buffered.tryGetReadBuffer() == nullptr) {
-        rr.type = RRType::NotValid;
+        eof = true;
         return rr;
     }
 
@@ -430,10 +431,12 @@
 }
 
 void
-writeExceptionCapnp(const exception &e, RRType type)
+writeExceptionCapnp(const exception &e, RRType type, RequestOrResponse::RpcId id)
 {
     capnp::MallocMessageBuilder message;
     piper::RpcResponse::Builder builder = message.initRoot<piper::RpcResponse>();
+
+    buildId(builder, id);
     VampnProto::buildRpcResponse_Exception(builder, e, type);
     
     writeMessageToFd(1, message);
@@ -472,6 +475,10 @@
     case RRType::Configure:
     {
         auto &creq = request.configurationRequest;
+        if (!creq.plugin) {
+            throw runtime_error("unknown plugin handle supplied to configure");
+        }
+            
         auto h = mapper.pluginToHandle(creq.plugin);
         if (mapper.isConfigured(h)) {
             throw runtime_error("plugin has already been configured");
@@ -490,6 +497,10 @@
     case RRType::Process:
     {
         auto &preq = request.processRequest;
+        if (!preq.plugin) {
+            throw runtime_error("unknown plugin handle supplied to process");
+        }
+
         auto h = mapper.pluginToHandle(preq.plugin);
         if (!mapper.isConfigured(h)) {
             throw runtime_error("plugin has not been configured");
@@ -521,6 +532,10 @@
     case RRType::Finish:
     {
         auto &freq = request.finishRequest;
+        if (!freq.plugin) {
+            throw runtime_error("unknown plugin handle supplied to finish");
+        }
+
         response.finishResponse.plugin = freq.plugin;
 
         auto h = mapper.pluginToHandle(freq.plugin);
@@ -547,13 +562,13 @@
 }
 
 RequestOrResponse
-readRequest(string format)
+readRequest(string format, bool &eof)
 {
     if (format == "capnp") {
-        return readRequestCapnp();
+        return readRequestCapnp(eof);
     } else if (format == "json") {
         string err;
-        auto result = readRequestJson(err);
+        auto result = readRequestJson(err, eof);
         if (err != "") throw runtime_error(err);
         else return result;
     } else {
@@ -576,13 +591,13 @@
 }
 
 void
-writeException(string format, const exception &e, RRType type)
+writeException(string format, const exception &e, RRType type, RequestOrResponse::RpcId id)
 {
     resumeOutput();
     if (format == "capnp") {
-        writeExceptionCapnp(e, type);
+        writeExceptionCapnp(e, type, id);
     } else if (format == "json") {
-        writeExceptionJson(e, type);
+        writeExceptionJson(e, type, id);
     } else {
         throw runtime_error("unknown output format \"" + format + "\"");
     }
@@ -644,10 +659,10 @@
         
         try {
 
-            request = readRequest(format);
+            bool eof = false;
+            request = readRequest(format, eof);
             
-            // NotValid without an exception indicates EOF:
-            if (request.type == RRType::NotValid) {
+            if (eof) {
                 if (debug) {
                     cerr << myname << " " << pid << ": eof reached, exiting" << endl;
                 }
@@ -659,7 +674,28 @@
                      << int(request.type)
                      << endl;
             }
+            
+        } catch (exception &e) {
 
+            if (debug) {
+                cerr << myname << " " << pid << ": error: " << e.what() << endl;
+            }
+
+            writeException(format, e, request.type, request.id);
+
+            if (format == "capnp") {
+                // Don't try to continue; we can't recover from a
+                // mangled input stream. However, we can return a
+                // successful error code because we are reporting the
+                // status in our Capnp output stream instead
+                if (debug) {
+                    cerr << myname << " " << pid << ": not attempting to recover from capnp parse problems, exiting" << endl;
+                }
+                exit(0);
+            }
+        }
+
+        try {
             RequestOrResponse response = handleRequest(request, debug);
             response.id = request.id;
 
@@ -689,18 +725,7 @@
                 cerr << myname << " " << pid << ": error: " << e.what() << endl;
             }
 
-            writeException(format, e, request.type);
-
-            if (format == "capnp") {
-                // Don't try to continue; we can't recover from a
-                // mangled input stream. However, we can return a
-                // successful error code because we are reporting the
-                // status in our Capnp output stream instead
-                if (debug) {
-                    cerr << myname << " " << pid << ": not attempting to recover from capnp parse problems, exiting" << endl;
-                }
-                exit(0);
-            }
+            writeException(format, e, request.type, request.id);
         }
     }
 
--- a/vamp-server/test.sh	Fri Jan 20 22:24:11 2017 +0000
+++ b/vamp-server/test.sh	Fri Jan 20 22:24:44 2017 +0000
@@ -2,8 +2,11 @@
 
 set -eu
 
-piperdir=../piper
-vampsdkdir=../vamp-plugin-sdk
+mydir=$(dirname "$0")
+
+piperdir="$mydir"/../../piper
+vampsdkdir="$mydir"/../../vamp-plugin-sdk
+bindir="$mydir"/../bin
 schemadir="$piperdir"/json/schema
 
 if [ ! -d "$schemadir" ]; then
@@ -51,23 +54,32 @@
     validate "$respfile" "rpcresponse"
 }
 
+# NB this list of commands includes a couple that are expected to fail
+# (process before configure, configure with nonexistent handle, finish
+# same handle twice)
 cat > "$input" <<EOF
 {"method":"list"}
 {"method":"list","params": {"from":["vamp-example-plugins","something-nonexistent"]}}
 {"method":"list","params": {"from":["something-nonexistent"]}}
 {"method":"load","id":6,"params": {"key":"vamp-example-plugins:percussiononsets","inputSampleRate":44100,"adapterFlags":["AdaptInputDomain","AdaptBufferSize"]}}
+{"method":"process","params": {"handle": 1, "processInput": { "timestamp": {"s": 0, "n": 0}, "inputBuffers": [ [1,2,3,4,5,6,7,8] ]}}}
 {"method":"configure","id":"weevil","params":{"handle":1,"configuration":{"blockSize": 8, "channelCount": 1, "parameterValues": {"sensitivity": 40, "threshold": 3}, "stepSize": 8}}}
+{"method":"configure","id":9,"params":{"handle":-9999,"configuration":{"blockSize": 8, "channelCount": 1, "parameterValues": {"sensitivity": 40, "threshold": 3}, "stepSize": 8}}}
 {"method":"process","params": {"handle": 1, "processInput": { "timestamp": {"s": 0, "n": 0}, "inputBuffers": [ [1,2,3,4,5,6,7,8] ]}}}
 {"method":"finish","params": {"handle": 1}}
+{"method":"finish","id":"blah","params": {"handle": 1}}
 EOF
 
 # Expected output, apart from the plugin list which seems a bit
 # fragile to check here
 cat > "$expected" <<EOF
 {"id": 6, "jsonrpc": "2.0", "method": "load", "result": {"defaultConfiguration": {"blockSize": 1024, "channelCount": 1, "parameterValues": {"sensitivity": 40, "threshold": 3}, "stepSize": 1024}, "handle": 1, "staticData": {"basic": {"description": "Detect percussive note onsets by identifying broadband energy rises", "identifier": "percussiononsets", "name": "Simple Percussion Onset Detector"}, "basicOutputInfo": [{"description": "Percussive note onset locations", "identifier": "onsets", "name": "Onsets"}, {"description": "Broadband energy rise detection function", "identifier": "detectionfunction", "name": "Detection Function"}], "category": ["Time", "Onsets"], "copyright": "Code copyright 2006 Queen Mary, University of London, after Dan Barry et al 2005.  Freely redistributable (BSD license)", "inputDomain": "TimeDomain", "key": "vamp-example-plugins:percussiononsets", "maker": "Vamp SDK Example Plugins", "maxChannelCount": 1, "minChannelCount": 1, "parameters": [{"basic": {"description": "Energy rise within a frequency bin necessary to count toward broadband total", "identifier": "threshold", "name": "Energy rise threshold"}, "defaultValue": 3, "extents": {"max": 20, "min": 0}, "unit": "dB", "valueNames": []}, {"basic": {"description": "Sensitivity of peak detector applied to broadband detection function", "identifier": "sensitivity", "name": "Sensitivity"}, "defaultValue": 40, "extents": {"max": 100, "min": 0}, "unit": "%", "valueNames": []}], "programs": [], "version": 2}}}
+{"error": {"code": 0, "message": "error in process request: plugin has not been configured"}, "jsonrpc": "2.0", "method": "process"}
 {"id": "weevil", "jsonrpc": "2.0", "method": "configure", "result": {"handle": 1, "outputList": [{"basic": {"description": "Percussive note onset locations", "identifier": "onsets", "name": "Onsets"}, "configured": {"binCount": 0, "binNames": [], "hasDuration": false, "sampleRate": 44100, "sampleType": "VariableSampleRate", "unit": ""}}, {"basic": {"description": "Broadband energy rise detection function", "identifier": "detectionfunction", "name": "Detection Function"}, "configured": {"binCount": 1, "binNames": [""], "hasDuration": false, "quantizeStep": 1, "sampleRate": 86.1328125, "sampleType": "FixedSampleRate", "unit": ""}}]}}
+{"error": {"code": 0, "message": "error in configure request: unknown plugin handle supplied to configure"}, "id": 9, "jsonrpc": "2.0", "method": "configure"}
 {"jsonrpc": "2.0", "method": "process", "result": {"features": {}, "handle": 1}}
 {"jsonrpc": "2.0", "method": "finish", "result": {"features": {"detectionfunction": [{"featureValues": [0], "timestamp": {"n": 11609977, "s": 0}}]}, "handle": 1}}
+{"error": {"code": 0, "message": "error in finish request: unknown plugin handle supplied to finish"}, "id": "blah", "jsonrpc": "2.0", "method": "finish"}
 EOF
 
 # We run the whole test twice, once with the server in Capnp mode
@@ -77,7 +89,7 @@
 #debugflag=-d
 debugflag=
 
-for format in json capnp ; do
+for format in json capnp ; do  # nb must be json first: see comment at end of loop
 
     ( export VAMP_PATH="$vampsdkdir"/examples ;
       while read request ; do
@@ -85,11 +97,11 @@
           echo "$request"
       done |
           if [ "$format" = "json" ]; then
-              bin/piper-vamp-simple-server $debugflag json
+              "$bindir"/piper-vamp-simple-server $debugflag json
           else
-              bin/piper-convert request -i json -o capnp |
-                  bin/piper-vamp-simple-server $debugflag capnp |
-                  bin/piper-convert response -i capnp -o json
+              "$bindir"/piper-convert request -i json -o capnp |
+                  "$bindir"/piper-vamp-simple-server $debugflag capnp |
+                  "$bindir"/piper-convert response -i capnp -o json
           fi |
           while read response ; do
               echo "$response" >> "$allrespfile"
@@ -102,7 +114,7 @@
 
     echo "Checking response contents against expected contents..."
     if ! cmp "$obtained" "$expected"; then
-        diff -u1 "$obtained" "$expected"
+        diff -U 1 "$obtained" "$expected"
     else
         echo "OK"
     fi
@@ -142,6 +154,9 @@
     
     rm "$allrespfile"
 
+    # The capnp output doesn't preserve the method name in error
+    # responses, so replace those now that we've done the json test
+    perl -i -p -e 's/(error.*"method": )"[^"]*"/$1"invalid"/' "$expected"
 done
 
 echo "Tests succeeded"  # set -e at top should ensure we don't get here otherwise