diff --git a/__snapshots__/read-rows.js b/__snapshots__/read-rows.js new file mode 100644 index 000000000..d320126df --- /dev/null +++ b/__snapshots__/read-rows.js @@ -0,0 +1,1734 @@ +exports['Bigtable/ReadRows with custom responses and createReadStream arguments should pass checks with a simple call 1'] = { + "input": { + "responses": [ + { + "row_keys": [ + "a", + "b", + "c" + ], + "last_row_key": "c", + "end_with_error": 13 + } + ], + "message": { + "rowKeys": [], + "rowRanges": [ + {} + ] + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [ + "a", + "b", + "c" + ] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0 + ], + "callCount": 1 + } + } +} + +exports['Bigtable/ReadRows with custom responses and createReadStream arguments retries a failed read 1'] = { + "input": { + "responses": [ + { + "row_keys": [ + "a", + "b" + ], + "last_row_key": "c", + "end_with_error": 4 + }, + { + "row_keys": [ + "c" + ], + "last_row_key": "c" + } + ], + "message": { + "rowKeys": [], + "rowRanges": [ + {} + ] + } + }, + "output": { + "results": { + "result": "end stream", + "data": [ + [ + "a", + "b" + ], + [ + "c" + ] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + }, + "1": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyOpen": { + "type": "Buffer", + "data": [ + 99 + ] + }, + "startKey": "startKeyOpen" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 1 + ], + "callCount": 2 + } + } +} + +exports['Bigtable/ReadRows with custom responses and createReadStream arguments fails after all available retries 1'] = { + "input": { + "responses": [ + { + "end_with_error": 4 + }, + { + "end_with_error": 4 + }, + { + "end_with_error": 4 + }, + { + "end_with_error": 4 + } + ], + "message": { + "rowKeys": [], + "rowRanges": [ + {} + ] + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with custom responses and createReadStream arguments resets the retry counter after a successful read 1'] = { + "input": { + "responses": [ + { + "row_keys": [ + "a" + ], + "last_row_key": "a", + "end_with_error": 4 + }, + { + "end_with_error": 4 + }, + { + "end_with_error": 4 + }, + { + "end_with_error": 4 + }, + { + "row_keys": [ + "b" + ], + "last_row_key": "b", + "end_with_error": 4 + }, + { + "end_with_error": 4 + }, + { + "end_with_error": 4 + }, + { + "row_keys": [ + "c" + ], + "last_row_key": "b" + } + ], + "message": { + "rowKeys": [], + "rowRanges": [ + {} + ] + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [ + "a" + ], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + }, + "1": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyOpen": { + "type": "Buffer", + "data": [ + 97 + ] + }, + "startKey": "startKeyOpen" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 1, + 1, + 1 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with custom responses and createReadStream arguments moves the start point of a range being consumed 1'] = { + "input": { + "responses": [ + { + "row_keys": [ + "a", + "b" + ], + "end_with_error": 4 + }, + { + "row_keys": [ + "c" + ] + } + ], + "message": { + "ranges": [ + { + "start": { + "value": "b", + "inclusive": false + }, + "end": "z" + } + ] + } + }, + "output": { + "results": { + "result": "end stream", + "data": [ + [ + "a", + "b" + ], + [ + "c" + ] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 97 + ] + }, + "startKey": "startKeyClosed", + "endKeyClosed": { + "type": "Buffer", + "data": [ + 122 + ] + }, + "endKey": "endKeyClosed" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + }, + "1": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyOpen": { + "type": "Buffer", + "data": [ + 98 + ] + }, + "startKey": "startKeyOpen", + "endKeyClosed": { + "type": "Buffer", + "data": [ + 122 + ] + }, + "endKey": "endKeyClosed" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 1 + ], + "callCount": 2 + } + } +} + +exports['Bigtable/ReadRows with custom responses and createReadStream arguments removes ranges already consumed 1'] = { + "input": { + "responses": [ + { + "row_keys": [ + "a", + "b", + "c" + ], + "end_with_error": 4 + }, + { + "row_keys": [ + "x" + ] + } + ], + "message": { + "ranges": [ + { + "start": "x", + "end": "z" + } + ] + } + }, + "output": { + "results": { + "result": "end stream", + "data": [ + [ + "a", + "b", + "c" + ], + [ + "x" + ] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 97 + ] + }, + "startKey": "startKeyClosed", + "endKeyClosed": { + "type": "Buffer", + "data": [ + 99 + ] + }, + "endKey": "endKeyClosed" + }, + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 120 + ] + }, + "startKey": "startKeyClosed", + "endKeyClosed": { + "type": "Buffer", + "data": [ + 122 + ] + }, + "endKey": "endKeyClosed" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + }, + "1": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 120 + ] + }, + "startKey": "startKeyClosed", + "endKeyClosed": { + "type": "Buffer", + "data": [ + 122 + ] + }, + "endKey": "endKeyClosed" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 1 + ], + "callCount": 2 + } + } +} + +exports['Bigtable/ReadRows with custom responses and createReadStream arguments removes keys already read 1'] = { + "input": { + "responses": [ + { + "row_keys": [ + "a", + "b", + "c" + ], + "end_with_error": 4 + }, + { + "row_keys": [ + "x" + ] + } + ], + "message": { + "keys": [ + "a", + "b", + "x" + ] + } + }, + "output": { + "results": { + "result": "end stream", + "data": [ + [ + "a", + "b", + "c" + ], + [ + "x" + ] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [ + { + "type": "Buffer", + "data": [ + 97 + ] + }, + { + "type": "Buffer", + "data": [ + 98 + ] + }, + { + "type": "Buffer", + "data": [ + 120 + ] + } + ], + "rowRanges": [] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + }, + "1": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [ + { + "type": "Buffer", + "data": [ + 120 + ] + } + ], + "rowRanges": [] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 1 + ], + "callCount": 2 + } + } +} + +exports['Bigtable/ReadRows with custom responses and createReadStream arguments adjust the limit based on the number of rows read 1'] = { + "input": { + "responses": [ + { + "row_keys": [ + "a", + "b" + ], + "end_with_error": 4 + }, + { + "row_keys": [ + "x" + ] + } + ], + "message": { + "limit": 10 + } + }, + "output": { + "results": { + "result": "end stream", + "data": [ + [ + "a", + "b" + ], + [ + "x" + ] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "10", + "appProfileId": "" + }, + "1": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyOpen": { + "type": "Buffer", + "data": [ + 98 + ] + }, + "startKey": "startKeyOpen" + } + ] + }, + "filter": null, + "rowsLimit": "8", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 1 + ], + "callCount": 2 + } + } +} + +exports['Bigtable/ReadRows with custom responses and createReadStream arguments respects the max retries parameter 1'] = { + "input": { + "responses": [ + { + "end_with_error": 4 + }, + { + "end_with_error": 4 + }, + { + "end_with_error": 4 + }, + { + "end_with_error": 4 + } + ], + "message": { + "maxRetries": 2 + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back where the error is retryable should ensure correct behavior with deadline exceeded error 1'] = { + "input": { + "code": 4, + "message": {} + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back where the error is retryable should ensure correct behavior with resource exhausted error 1'] = { + "input": { + "code": 8, + "message": {} + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back where the error is retryable should ensure correct behavior with aborted error 1'] = { + "input": { + "code": 10, + "message": {} + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back where the error is retryable should ensure correct behavior with unavailable error 1'] = { + "input": { + "code": 14, + "message": {} + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back where the error is not retryable should ensure correct behavior with cancelled error 1'] = { + "input": { + "code": 1, + "message": {} + }, + "output": { + "results": { + "result": "end stream", + "data": [ + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0 + ], + "callCount": 1 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back where the error is not retryable should ensure correct behavior with internal error 1'] = { + "input": { + "code": 13, + "message": {} + }, + "output": { + "results": { + "result": "error", + "data": [ + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0 + ], + "callCount": 1 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back where the error is not retryable should ensure correct behavior with invalid argument error 1'] = { + "input": { + "code": 3, + "message": {} + }, + "output": { + "results": { + "result": "error", + "data": [ + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0 + ], + "callCount": 1 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with an empty request 1'] = { + "input": { + "code": 4, + "message": {} + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with a decode value set 1'] = { + "input": { + "code": 4, + "message": { + "decode": true + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with an encoding value set 1'] = { + "input": { + "code": 4, + "message": { + "encoding": "test-encoding" + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with start and end values 1'] = { + "input": { + "code": 4, + "message": { + "start": "test-start", + "end": "test-end" + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 115, + 116, + 97, + 114, + 116 + ] + }, + "startKey": "startKeyClosed", + "endKeyClosed": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 101, + 110, + 100 + ] + }, + "endKey": "endKeyClosed" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with keys 1'] = { + "input": { + "code": 4, + "message": { + "keys": [ + "test-key-1", + "test-key-2" + ] + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [ + { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 107, + 101, + 121, + 45, + 49 + ] + }, + { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 107, + 101, + 121, + 45, + 50 + ] + } + ], + "rowRanges": [] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with a filter 1'] = { + "input": { + "code": 4, + "message": { + "filter": [ + { + "column": "columnPrefix" + } + ] + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": { + "columnQualifierRegexFilter": { + "type": "Buffer", + "data": [ + 99, + 111, + 108, + 117, + 109, + 110, + 80, + 114, + 101, + 102, + 105, + 120 + ] + }, + "filter": "columnQualifierRegexFilter" + }, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with a limit 1'] = { + "input": { + "code": 4, + "message": { + "limit": 10 + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + {} + ] + }, + "filter": null, + "rowsLimit": "10", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with a prefix 1'] = { + "input": { + "code": 4, + "message": { + "prefix": "test-prefix" + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 112, + 114, + 101, + 102, + 105, + 120 + ] + }, + "startKey": "startKeyClosed", + "endKeyOpen": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 112, + 114, + 101, + 102, + 105, + 121 + ] + }, + "endKey": "endKeyOpen" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with prefixes 1'] = { + "input": { + "code": 4, + "message": { + "prefixes": [ + "test-prefix1", + "test-prefix2" + ] + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 112, + 114, + 101, + 102, + 105, + 120, + 49 + ] + }, + "startKey": "startKeyClosed", + "endKeyOpen": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 112, + 114, + 101, + 102, + 105, + 120, + 50 + ] + }, + "endKey": "endKeyOpen" + }, + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 112, + 114, + 101, + 102, + 105, + 120, + 50 + ] + }, + "startKey": "startKeyClosed", + "endKeyOpen": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 112, + 114, + 101, + 102, + 105, + 120, + 51 + ] + }, + "endKey": "endKeyOpen" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} + +exports['Bigtable/ReadRows with a mock server that always sends an error back with a deadline exceeded error and different createReadStream arguments should pass checks with a list of ranges 1'] = { + "input": { + "code": 4, + "message": { + "ranges": [ + { + "start": "test-start-1", + "end": "test-end-1" + }, + { + "start": "test-start-2", + "end": "test-end-2" + } + ] + } + }, + "output": { + "results": { + "result": "error", + "data": [ + [], + [], + [], + [] + ] + }, + "requestData": { + "requests": { + "0": { + "tableName": "projects/{{projectId}}/instances/fake-instance/tables/fake-table", + "rows": { + "rowKeys": [], + "rowRanges": [ + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 115, + 116, + 97, + 114, + 116, + 45, + 49 + ] + }, + "startKey": "startKeyClosed", + "endKeyClosed": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 101, + 110, + 100, + 45, + 49 + ] + }, + "endKey": "endKeyClosed" + }, + { + "startKeyClosed": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 115, + 116, + 97, + 114, + 116, + 45, + 50 + ] + }, + "startKey": "startKeyClosed", + "endKeyClosed": { + "type": "Buffer", + "data": [ + 116, + 101, + 115, + 116, + 45, + 101, + 110, + 100, + 45, + 50 + ] + }, + "endKey": "endKeyClosed" + } + ] + }, + "filter": null, + "rowsLimit": "0", + "appProfileId": "" + } + }, + "requestOrder": [ + 0, + 0, + 0, + 0 + ], + "callCount": 4 + } + } +} diff --git a/package.json b/package.json index 75294fc01..6f49c6c5c 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@google-cloud/promisify": "^3.0.0", "arrify": "^2.0.0", "concat-stream": "^2.0.0", + "deep-equal": "^2.0.5", "dot-prop": "^6.0.0", "escape-string-regexp": "^4.0.0", "extend": "^3.0.2", @@ -89,6 +90,7 @@ "sinon": "^14.0.0", "tcp-port-used": "^1.0.2", "snap-shot-it": "^7.9.1", + "tcp-port-used": "^1.0.2", "ts-loader": "^9.0.0", "typescript": "^4.6.4", "uuid": "^8.0.0", diff --git a/test/errors.ts b/test/errors.ts index 33721b743..f7b780600 100644 --- a/test/errors.ts +++ b/test/errors.ts @@ -21,9 +21,9 @@ import {Bigtable} from '../src'; import * as assert from 'assert'; import {GoogleError, grpc, ServiceError} from 'google-gax'; -import {MockServer} from '../src/util/mock-servers/mock-server'; -import {BigtableClientMockService} from '../src/util/mock-servers/service-implementations/bigtable-client-mock-service'; -import {MockService} from '../src/util/mock-servers/mock-service'; +import {MockServer} from './helpers/mock-servers/mock-server'; +import {BigtableClientMockService} from './helpers/mock-servers/service-implementations/bigtable-client-mock-service'; +import {MockService} from './helpers/mock-servers/mock-service'; function isServiceError(error: any): error is ServiceError { return ( diff --git a/src/util/mock-servers/mock-server.ts b/test/helpers/mock-servers/mock-server.ts similarity index 100% rename from src/util/mock-servers/mock-server.ts rename to test/helpers/mock-servers/mock-server.ts diff --git a/src/util/mock-servers/mock-service.ts b/test/helpers/mock-servers/mock-service.ts similarity index 100% rename from src/util/mock-servers/mock-service.ts rename to test/helpers/mock-servers/mock-service.ts diff --git a/src/util/mock-servers/service-implementations/bigtable-client-mock-service.ts b/test/helpers/mock-servers/service-implementations/bigtable-client-mock-service.ts similarity index 100% rename from src/util/mock-servers/service-implementations/bigtable-client-mock-service.ts rename to test/helpers/mock-servers/service-implementations/bigtable-client-mock-service.ts diff --git a/test/helpers/mock-servers/service-testers/service-handlers/implementation/read-rows-handler.ts b/test/helpers/mock-servers/service-testers/service-handlers/implementation/read-rows-handler.ts new file mode 100644 index 000000000..9f2b8d339 --- /dev/null +++ b/test/helpers/mock-servers/service-testers/service-handlers/implementation/read-rows-handler.ts @@ -0,0 +1,126 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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 file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {SameCallHandler} from './same-call-handler'; +import {MockService} from '../../../mock-service'; +import {Mutation} from '../../../../../../src/mutation'; + +function rowResponse(rowKey: {}) { + return { + rowKey: Mutation.convertToBytes(rowKey), + familyName: {value: 'family'}, + qualifier: {value: Mutation.convertToBytes('qualifier')}, + valueSize: 0, + timestampMicros: 0, + labels: [], + commitRow: true, + value: Mutation.convertToBytes('value'), + }; +} + +export interface ReadRowsResponse { + row_keys?: string[]; + last_row_key?: string; + end_with_error?: number; +} + +export class ReadRowsHandler extends SameCallHandler { + responses: ReadRowsResponse[]; + request: any = null; + callCount = 0; + message: any; + + // TODO: service and endpoint should be bundled into one object. + constructor( + service: MockService, + endpoint: string, + responses: ReadRowsResponse[], + message?: any + ) { + super(service, endpoint); + this.responses = responses; + this.message = Object.assign({}, message); + } + + // TODO: Create interface for this. + callHandler(call: any) { + const lastResponse = this.responses[this.callCount - 1]; + if (lastResponse && lastResponse.row_keys) { + const grpcResponse = { + chunks: lastResponse.row_keys.map(rowResponse), + lastScannedRowKey: Mutation.convertToBytes(lastResponse.last_row_key), + }; + call.write(grpcResponse); + } + // Set a timer and send an error if we are confident that all data has been sent back to the user + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + const endRequest = (lastResponse: any) => { + const errorCode = lastResponse.end_with_error; + if (errorCode) { + call.emit('error', { + code: errorCode, + details: 'Details for a particular type of error', + }); + } else { + call.end(); + } + }; + const checkCollected = () => { + // Send the error if all data was collected + const lastIndex = self.data.length - 1; + const lastResponse = self.responses[self.callCount - 1]; + if (lastResponse) { + if (lastResponse.row_keys) { + if (self.data[lastIndex].length === lastResponse.row_keys.length) { + endRequest(lastResponse); + } else { + startTimer(); + } + } else { + endRequest(lastResponse); + } + } else { + throw Error('Ran out of requests to send'); + } + }; + const startTimer = () => { + setTimeout(checkCollected, 2500); + }; + startTimer(); + } + + addData(data: string) { + // Add data collected from the stream + const lastIndex = this.data.length - 1; + this.data[lastIndex].push(data); + } + + snapshot(results: any): any { + return { + input: Object.assign( + {responses: this.responses}, + this.message ? {message: this.message} : null + ), + output: { + results, + requestData: this.requests(), + }, + }; + } +} diff --git a/test/helpers/mock-servers/service-testers/service-handlers/implementation/same-call-handler.ts b/test/helpers/mock-servers/service-testers/service-handlers/implementation/same-call-handler.ts new file mode 100644 index 000000000..1bcb30d30 --- /dev/null +++ b/test/helpers/mock-servers/service-testers/service-handlers/implementation/same-call-handler.ts @@ -0,0 +1,74 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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 file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {MockService} from '../../../mock-service'; +import {ServiceHandler} from '../service-handler'; + +const equal = require('deep-equal'); + +export abstract class SameCallHandler extends ServiceHandler { + service: MockService; + request: any = null; + requestList: any[] = []; + requestOrder: number[] = []; + callCount = 0; + endpoint: string; + data: string[][] = []; + + protected constructor(service: MockService, endpoint: string) { + super(); + this.endpoint = endpoint; + this.service = service; + } + + setupService(): void { + const handleRpcCall = (call: any) => { + // TODO: Make an abstraction of this + const callRequest = call.request; + const requestIndex = this.requestList.findIndex(request => { + return equal(request, callRequest); + }); + if (requestIndex === -1) { + this.requestList.push(callRequest); + this.requestOrder.push(this.requestList.length - 1); + } else { + this.requestOrder.push(requestIndex); + } + this.request = callRequest; + this.callCount++; + this.data.push([]); + this.callHandler(call); + }; + this.service.setService({ + // Abstraction: Always emit error + [this.endpoint]: handleRpcCall, + }); + } + + getData() { + return this.data; + } + + requests() { + return { + requests: Object.assign({}, this.requestList), + requestOrder: this.requestOrder, + callCount: this.callCount, + }; + } +} diff --git a/test/helpers/mock-servers/service-testers/service-handlers/implementation/send-error-handler.ts b/test/helpers/mock-servers/service-testers/service-handlers/implementation/send-error-handler.ts new file mode 100644 index 000000000..7aa9545e5 --- /dev/null +++ b/test/helpers/mock-servers/service-testers/service-handlers/implementation/send-error-handler.ts @@ -0,0 +1,66 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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 file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {grpc} from 'google-gax'; +import {SameCallHandler} from './same-call-handler'; +import {MockService} from '../../../mock-service'; +import {Row} from '../../../../../../src/row'; + +export class SendErrorHandler extends SameCallHandler { + code: grpc.status; + request: any = null; + callCount = 0; + message: any; + + // TODO: service and endpoint should be bundled into one object. + constructor( + service: MockService, + endpoint: string, + code: grpc.status, + message?: any + ) { + super(service, endpoint); + this.code = code; + this.message = Object.assign({}, message); + } + + callHandler(call: any) { + call.emit('error', { + code: this.code, + details: 'Details for a particular type of error', + }); + } + + addData(data: string) { + const lastIndex = this.data.length - 1; + this.data[lastIndex].push(data); + } + + snapshot(results: any): any { + return { + input: Object.assign( + {code: this.code}, + this.message ? {message: this.message} : null + ), + output: { + results, + requestData: this.requests(), + }, + }; + } +} diff --git a/test/helpers/mock-servers/service-testers/service-handlers/service-handler.ts b/test/helpers/mock-servers/service-testers/service-handlers/service-handler.ts new file mode 100644 index 000000000..742316b25 --- /dev/null +++ b/test/helpers/mock-servers/service-testers/service-handlers/service-handler.ts @@ -0,0 +1,48 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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 file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +export abstract class ServiceHandler { + /* + callHandler accepts a grpc call and provides behaviour for that grpc call + which may involve sending errors or data back to the client for + example. + */ + abstract callHandler(call: any): void; + + /* + snapshotOutput is used to provide a custom json object that represents the + results of the test that was run with this service handler. + */ + abstract snapshot(results: any): any; + + /* + setupService is called to setup the service we use for collecting data about + a running test. + */ + abstract setupService(): void; + + /* + addData is called to add data which will be reported in the snapshot later on. + */ + abstract addData(data: string): void; + + /* + getData is called to get all data which was collected from requests. + */ + abstract getData(): string[][]; +} diff --git a/test/helpers/mock-servers/service-testers/stream-fetchers/implementation/read-rows-fetcher.ts b/test/helpers/mock-servers/service-testers/stream-fetchers/implementation/read-rows-fetcher.ts new file mode 100644 index 000000000..b44ed7901 --- /dev/null +++ b/test/helpers/mock-servers/service-testers/stream-fetchers/implementation/read-rows-fetcher.ts @@ -0,0 +1,36 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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 file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {GetRowsOptions, Table} from '../../../../../../src/table'; +import internal = require('stream'); +import {StreamFetcher} from '../stream-fetcher'; + +export class ReadRowsFetcher extends StreamFetcher { + table: Table; + opts: GetRowsOptions; + + constructor(table: Table, opts?: GetRowsOptions) { + super(); + this.opts = opts ?? {}; + this.table = table; + } + + fetchStream(): internal.PassThrough { + return this.table.createReadStream(this.opts); + } +} diff --git a/test/helpers/mock-servers/service-testers/stream-fetchers/stream-fetcher.ts b/test/helpers/mock-servers/service-testers/stream-fetchers/stream-fetcher.ts new file mode 100644 index 000000000..1b9795af5 --- /dev/null +++ b/test/helpers/mock-servers/service-testers/stream-fetchers/stream-fetcher.ts @@ -0,0 +1,23 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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 file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import internal = require('stream'); + +export abstract class StreamFetcher { + abstract fetchStream(): internal.PassThrough; +} diff --git a/test/helpers/mock-servers/service-testers/stream-tester.ts b/test/helpers/mock-servers/service-testers/stream-tester.ts new file mode 100644 index 000000000..d52de38e4 --- /dev/null +++ b/test/helpers/mock-servers/service-testers/stream-tester.ts @@ -0,0 +1,59 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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 file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {ServiceHandler} from './service-handlers/service-handler'; +import {StreamFetcher} from './stream-fetchers/stream-fetcher'; +import {ServiceError} from 'google-gax'; + +import * as snapshot from 'snap-shot-it'; +import {Row} from '../../../../src/row'; + +export class StreamTester { + serviceHandler: ServiceHandler; + streamFetcher: StreamFetcher; + + constructor(serviceHandler: ServiceHandler, streamFetcher: StreamFetcher) { + this.serviceHandler = serviceHandler; + this.streamFetcher = streamFetcher; + } + + checkSnapshots(callback: () => void): void { + const collectSnapshot = (results: any) => { + snapshot(this.serviceHandler.snapshot(results)); + callback(); + }; + const getData = (result: string) => { + return { + result, + data: this.serviceHandler.getData(), + }; + }; + this.serviceHandler.setupService(); + const fetchedStream = this.streamFetcher.fetchStream(); + fetchedStream + .on('error', (error: ServiceError) => { + collectSnapshot(getData('error')); + }) + .on('data', (message: Row) => { + this.serviceHandler.addData(message.id); + }) + .on('end', (message: Row) => { + collectSnapshot(getData('end stream')); + }); + } +} diff --git a/test/util/mock-server.ts b/test/mock-server.ts similarity index 96% rename from test/util/mock-server.ts rename to test/mock-server.ts index 920673579..56abc81a7 100644 --- a/test/util/mock-server.ts +++ b/test/mock-server.ts @@ -17,7 +17,7 @@ // ** All changes to this file may be overwritten. ** import {describe, it} from 'mocha'; -import {MockServer} from '../../src/util/mock-servers/mock-server'; +import {MockServer} from './helpers/mock-servers/mock-server'; import * as assert from 'assert'; const tcpPortUsed = require('tcp-port-used'); diff --git a/test/server/read-rows.ts b/test/server/read-rows.ts new file mode 100644 index 000000000..e1ddf7c2d --- /dev/null +++ b/test/server/read-rows.ts @@ -0,0 +1,318 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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 file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {before, describe, it} from 'mocha'; +import {Bigtable, GetRowsOptions} from '../../src'; + +import {grpc} from 'google-gax'; +import {MockServer} from '../helpers/mock-servers/mock-server'; +import {BigtableClientMockService} from '../helpers/mock-servers/service-implementations/bigtable-client-mock-service'; +import {MockService} from '../helpers/mock-servers/mock-service'; +import {SendErrorHandler} from '../helpers/mock-servers/service-testers/service-handlers/implementation/send-error-handler'; +import {ReadRowsFetcher} from '../helpers/mock-servers/service-testers/stream-fetchers/implementation/read-rows-fetcher'; +import {StreamTester} from '../helpers/mock-servers/service-testers/stream-tester'; +import {ServiceHandler} from '../helpers/mock-servers/service-testers/service-handlers/service-handler'; +import {Table} from '../../src/table'; +import { + ReadRowsHandler, + ReadRowsResponse, +} from '../helpers/mock-servers/service-testers/service-handlers/implementation/read-rows-handler'; +import {testGaxOptions} from './test-options'; + +describe('Bigtable/ReadRows', () => { + let server: MockServer; + let service: MockService; + let bigtable: Bigtable; + let table: Table; + + before(done => { + server = new MockServer(() => { + bigtable = new Bigtable({ + apiEndpoint: `localhost:${server.port}`, + }); + // TODO: Replace this with generated Ids so that we don't have flaky tests + table = bigtable.instance('fake-instance').table('fake-table'); + service = new BigtableClientMockService(server); + done(); + }); + }); + + function getStreamTester( + serviceHandler: ServiceHandler, + opts?: GetRowsOptions + ) { + const streamFetcher = new ReadRowsFetcher(table, opts); + return new StreamTester(serviceHandler, streamFetcher); + } + describe('with a mock server that always sends an error back', () => { + function checkRetryWithServer(code: grpc.status, callback: () => void) { + const serviceHandler = new SendErrorHandler(service, 'ReadRows', code); + const streamTester = getStreamTester(serviceHandler); + streamTester.checkSnapshots(callback); + } + describe('where the error is retryable', () => { + it('should ensure correct behavior with deadline exceeded error', done => { + checkRetryWithServer(grpc.status.DEADLINE_EXCEEDED, done); + }); + it('should ensure correct behavior with resource exhausted error', done => { + checkRetryWithServer(grpc.status.RESOURCE_EXHAUSTED, done); + }); + it('should ensure correct behavior with aborted error', done => { + checkRetryWithServer(grpc.status.ABORTED, done); + }); + it('should ensure correct behavior with unavailable error', done => { + checkRetryWithServer(grpc.status.UNAVAILABLE, done); + }); + }); + describe('where the error is not retryable', () => { + it('should ensure correct behavior with cancelled error', done => { + checkRetryWithServer(grpc.status.CANCELLED, done); + }); + it('should ensure correct behavior with internal error', done => { + checkRetryWithServer(grpc.status.INTERNAL, done); + }); + it('should ensure correct behavior with invalid argument error', done => { + checkRetryWithServer(grpc.status.INVALID_ARGUMENT, done); + }); + }); + describe('with a deadline exceeded error and different createReadStream arguments', () => { + function getServiceHandler(message: any) { + return new SendErrorHandler( + service, + 'ReadRows', + grpc.status.DEADLINE_EXCEEDED, + message + ); + } + function checkWithOptions(opts: any, callback: () => void) { + const serviceHandler = getServiceHandler(opts); + const streamTester = getStreamTester(serviceHandler, opts); + streamTester.checkSnapshots(callback); + } + it('should pass checks with an empty request', done => { + checkWithOptions({}, done); + }); + it('should pass checks with a decode value set', done => { + checkWithOptions({decode: true}, done); + }); + it('should pass checks with an encoding value set', done => { + // TODO: encoding + checkWithOptions({encoding: 'test-encoding'}, done); + }); + it('should pass checks with start and end values', done => { + checkWithOptions( + { + start: 'test-start', + end: 'test-end', + }, + done + ); + }); + it('should pass checks with keys', done => { + checkWithOptions({keys: ['test-key-1', 'test-key-2']}, done); + }); + it('should pass checks with a filter', done => { + checkWithOptions({filter: [{column: 'columnPrefix'}]}, done); + }); + it('should pass checks with a limit', done => { + checkWithOptions({limit: 10}, done); + }); + it('should pass checks with a prefix', done => { + checkWithOptions({prefix: 'test-prefix'}, done); + }); + it('should pass checks with prefixes', done => { + checkWithOptions({prefixes: ['test-prefix1', 'test-prefix2']}, done); + }); + it('should pass checks with a list of ranges', done => { + checkWithOptions( + { + ranges: [ + { + start: 'test-start-1', + end: 'test-end-1', + }, + { + start: 'test-start-2', + end: 'test-end-2', + }, + ], + }, + done + ); + }); + /* + it('should pass checks with gaxOptions', done => { + // TODO: Add the retry parameter + checkWithOptions( + { + gaxOptions: testGaxOptions, + }, + done + ); + }); + */ + }); + }); + describe('with custom responses and createReadStream arguments', () => { + function getServiceHandler(responses: ReadRowsResponse[], message: any) { + return new ReadRowsHandler(service, 'ReadRows', responses, message); + } + function checkWithOptions( + responses: ReadRowsResponse[], + opts: any, + callback: () => void + ) { + const serviceHandler = getServiceHandler(responses, opts); + const streamTester = getStreamTester(serviceHandler, opts); + streamTester.checkSnapshots(callback); + } + it('should pass checks with a simple call', done => { + checkWithOptions( + [ + { + row_keys: ['a', 'b', 'c'], + last_row_key: 'c', + end_with_error: grpc.status.INTERNAL, + }, + ], + { + rowKeys: [], + rowRanges: [{}], + }, + done + ); + }); + it('retries a failed read', done => { + checkWithOptions( + [ + { + row_keys: ['a', 'b'], + last_row_key: 'c', + end_with_error: grpc.status.DEADLINE_EXCEEDED, + }, + { + row_keys: ['c'], + last_row_key: 'c', + }, + ], + { + rowKeys: [], + rowRanges: [{}], + }, + done + ); + }); + it('fails after all available retries', done => { + checkWithOptions( + Array(4).fill({ + end_with_error: grpc.status.DEADLINE_EXCEEDED, + }), + { + rowKeys: [], + rowRanges: [{}], + }, + done + ); + }); + it('resets the retry counter after a successful read', done => { + checkWithOptions( + [ + {row_keys: ['a'], last_row_key: 'a', end_with_error: 4}, + {end_with_error: 4}, + {end_with_error: 4}, + {end_with_error: 4}, + {row_keys: ['b'], last_row_key: 'b', end_with_error: 4}, + {end_with_error: 4}, + {end_with_error: 4}, + {row_keys: ['c'], last_row_key: 'b'}, + ], + { + rowKeys: [], + rowRanges: [{}], + }, + done + ); + }); + it('moves the start point of a range being consumed', done => { + checkWithOptions( + [{row_keys: ['a', 'b'], end_with_error: 4}, {row_keys: ['c']}], + { + ranges: [ + { + start: 'a', + end: 'z', + }, + ], + }, + done + ); + }); + it('removes ranges already consumed', done => { + checkWithOptions( + [{row_keys: ['a', 'b', 'c'], end_with_error: 4}, {row_keys: ['x']}], + { + ranges: [ + { + start: 'a', + end: 'c', + }, + { + start: 'x', + end: 'z', + }, + ], + }, + done + ); + }); + it('removes keys already read', done => { + checkWithOptions( + [{row_keys: ['a', 'b', 'c'], end_with_error: 4}, {row_keys: ['x']}], + { + keys: ['a', 'b', 'x'], + }, + done + ); + }); + it('adjust the limit based on the number of rows read', done => { + checkWithOptions( + [{row_keys: ['a', 'b'], end_with_error: 4}, {row_keys: ['x']}], + {limit: 10}, + done + ); + }); + it('respects the max retries parameter', done => { + // NOTE: This test snapshot currently shows that maxRetries is not respected on a per call basis because three requests are required. + checkWithOptions( + Array(4).fill({ + end_with_error: grpc.status.DEADLINE_EXCEEDED, + }), + { + maxRetries: 2, + }, + done + ); + }); + }); + after(async () => { + server.shutdown(() => {}); + }); +}); + +// TODO: Think of interesting cases for the shouldRetryFn +// TODO: Consider setting up the framework so that we take snapshots of values passed into createReadStream afterwards diff --git a/test/server/test-options.ts b/test/server/test-options.ts new file mode 100644 index 000000000..1b1659b0c --- /dev/null +++ b/test/server/test-options.ts @@ -0,0 +1,68 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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 file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +export const testGaxOptions = { + timeout: 1000, + retry: { + retryCodes: [3, 5, 7, 12], + backoffSettings: { + maxRetries: 41, + initialRetryDelayMillis: 401, + retryDelayMultiplier: 42, + maxRetryDelayMillis: 402, + initialRpcTimeoutMillis: 403, + maxRpcTimeoutMillis: 404, + rpcTimeoutMultiplier: 43, + }, + }, + autoPaginate: true, + maxResults: 23, + maxRetries: 24, + otherArgs: { + otherArg1: 217, + }, + bundleOptions: { + elementCountLimit: 71, + requestByteLimit: 72, + elementCountThreshold: 73, + requestByteThreshold: 74, + delayThreshold: 75, + }, + isBundling: false, + longRunning: { + maxRetries: 5, + initialRetryDelayMillis: 501, + retryDelayMultiplier: 52, + maxRetryDelayMillis: 502, + initialRpcTimeoutMillis: 503, + maxRpcTimeoutMillis: 504, + totalTimeoutMillis: 505, + rpcTimeoutMultiplier: 53, + }, + apiName: 'test-apiName', + retryRequestOptions: { + objectMode: true, + request: 'test-request', + retries: 38, + noResponseRetries: 39, + currentRetryAttempt: 35, + shouldRetryFn: () => { + return true; + }, + }, +};