From a4835eb80cb4e135331644b7ab035616a81ab53a Mon Sep 17 00:00:00 2001 From: Krishnan Mahadevan Date: Sun, 20 Nov 2022 11:07:13 +0530 Subject: [PATCH 1/2] Adding test project to support Unit tests --- .../protobins/test-app-protos.protobin | Bin 0 -> 1799 bytes .../test-app-protos/pom.xml | 150 ++++++++++++++++++ .../com/github/thinkerou/LocalGrpcServer.java | 40 +++++ .../main/java/com/github/thinkerou/Main.java | 12 ++ .../com/github/thinkerou/StockService.java | 108 +++++++++++++ .../src/main/resources/sample.proto | 17 ++ .../src/main/resources/stockquotes.proto | 9 ++ .../src/main/resources/stocks.proto | 21 +++ 8 files changed, 357 insertions(+) create mode 100644 karate-grpc-core/artifacts/protobins/test-app-protos.protobin create mode 100644 karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/pom.xml create mode 100644 karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/LocalGrpcServer.java create mode 100644 karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/Main.java create mode 100644 karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/StockService.java create mode 100644 karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/sample.proto create mode 100644 karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/stockquotes.proto create mode 100644 karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/stocks.proto diff --git a/karate-grpc-core/artifacts/protobins/test-app-protos.protobin b/karate-grpc-core/artifacts/protobins/test-app-protos.protobin new file mode 100644 index 0000000000000000000000000000000000000000..74c60bfaa1d235d565f1de0a1f4bcd8e22a29885 GIT binary patch literal 1799 zcmb_dJ#Q015Y5@K6_1?{mIxvTL9jtG2%RIRL4*d25F{+)oJo*qth@92viN*^+1oR6 zMb9swqM)Rt;`i_m=qTBb^9ONaiqLuAzL|M5_ht=W47e6?>h{lK%5h|djB<)LT{)a@s_b;R18ZW&|N6dUJtWQ5Dy&8wo>9UW91dr zR1n&Md>2P9BOxa=*h&jV&YFelsg~hnm6AR9wG0I%aG|#1d=sTJ$SAp}46B^DqK8g2 z8aUKfigQ*$gKdRL;iBo#4o0289$+P0$V#xLfc`DK_9G2wI-(Y%L(;_z9RrF3n~e@J zpWdQv^P(x6ic#EIaH-~BT0-yPeuUW&v$m+x<_y~d5_qCWgG4ihqAOS0HP8Smu1|2l z&(~^mn~QS9jb8VZ^sASv7$}4X4(SqxUFi|qKi^$6o#X$gw`=_6xt|%h@KkQR98r=bBPjXW%{irdj!Ak!*U_R&Ul=lp&8pb>F6D|SzAP>I>QK+Za-Rg< z(mv!S3}ixfD5pC9qL&RlVLjZ2++>Jo4WN14Xmn1VHI5H9mg*njx>|c>uZe-`dEJb7 zGB|A+@2ZJsJnGwlXHw=(oybQaj;j5Deh3m*hR$!p_?k)|w(8$fw-0q{l}dQie2abm zm1Mt>fs=>zQ%a249)vz#^yn8CE<$M?R!=Y^y^)2_#DI9|0M^Dp6w)BVXb)B*I>66u vZYv{S{w1vmvF1RbK|K<*#rr1yV~_ + + + 4.0.0 + + com.github.thinkerou + test-app-protos + 1.0-SNAPSHOT + + test-app-protos + + http://www.example.com + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + 0.6.1 + 1.41.0 + + + + + + io.grpc + grpc-stub + ${protobuf.version} + + + + io.grpc + grpc-protobuf + ${protobuf.version} + + + io.grpc + grpc-services + ${protobuf.version} + + + io.grpc + grpc-netty + ${protobuf.version} + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + server + + + + kr.motd.maven + os-maven-plugin + 1.7.0 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + UTF-8 + ${java.version} + ${java.version} + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf.maven.plugin} + + true + com.google.protobuf:protoc:3.5.1:exe:${os.detected.classifier} + + grpc-java + io.grpc:protoc-gen-grpc-java:1.12.0:exe:${os.detected.classifier} + + src/main/resources + ${project.name}.protobin + ${project.build.outputDirectory} + true + + + + + compile + compile-custom + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + generate-sources + + add-source + + + + ${basedir}/target/generated-sources + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + + package + + shade + + + false + server + + + + com.github.thinkerou.Main + + + + + + + + + diff --git a/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/LocalGrpcServer.java b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/LocalGrpcServer.java new file mode 100644 index 0000000..8125b10 --- /dev/null +++ b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/LocalGrpcServer.java @@ -0,0 +1,40 @@ +package com.github.thinkerou; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.protobuf.services.ProtoReflectionService; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +//Sample format for interaction +//grpcurl --plaintext -d '{"ticker_symbol":"MSFT","company_name":"Microsoft Corp","description":"Microsoft company"}' \ +//localhost:8080 com.github.thinkerou.StockQuoteProvider/unaryGetStockQuote + +public class LocalGrpcServer { + + private final Server server; + + public LocalGrpcServer(int port) { + server = ServerBuilder.forPort(port) + .addService(new StockService()) + .addService(ProtoReflectionService.newInstance()) + .build(); + } + + public void start() { + try { + server.start(); + server.awaitTermination(); + }catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void stop() { + try { + server.shutdownNow().awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/Main.java b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/Main.java new file mode 100644 index 0000000..3f426ca --- /dev/null +++ b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/Main.java @@ -0,0 +1,12 @@ +package com.github.thinkerou; + +public class Main { + + public static void main(String[] args) { + int port = Integer.parseInt(System.getProperty("server.port", "8080")); + LocalGrpcServer server = new LocalGrpcServer(port); + Runnable task = server::stop; + Runtime.getRuntime().addShutdownHook(new Thread(task)); + server.start(); + } +} diff --git a/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/StockService.java b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/StockService.java new file mode 100644 index 0000000..ea2e654 --- /dev/null +++ b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/java/com/github/thinkerou/StockService.java @@ -0,0 +1,108 @@ +package com.github.thinkerou; + +import com.github.thinkerou.StockQuoteProviderGrpc.StockQuoteProviderImplBase; +import io.grpc.stub.StreamObserver; +import java.util.concurrent.ThreadLocalRandom; +import java.util.logging.Logger; +import java.util.stream.IntStream; + +public class StockService extends StockQuoteProviderImplBase { + + private static final Logger logger = Logger.getLogger(StockService.class.getName()); + + @Override + public void unaryGetStockQuote(Stock request, StreamObserver responseObserver) { + StockQuote stock = StockQuote.newBuilder() + .setPrice(fetchStockPriceBid(request)) + .setOfferNumber(1) + .setDescription("Price for stock:" + request.getTickerSymbol()) + .build(); + responseObserver.onNext(stock); + responseObserver.onCompleted(); + } + + @Override + public void serverSideStreamingGetListStockQuotes(Stock request, + StreamObserver responseObserver) { + IntStream.rangeClosed(1, 5) + .forEach(i -> { + StockQuote stockQuote = StockQuote.newBuilder() + .setPrice(fetchStockPriceBid(request)) + .setOfferNumber(i) + .setDescription("Price for stock:" + request.getTickerSymbol()) + .build(); + responseObserver.onNext(stockQuote); + }); + responseObserver.onCompleted(); + } + + @Override + public StreamObserver clientSideStreamingGetStatisticsOfStocks( + StreamObserver responseObserver) { + return new StreamObserver() { + private int count; + private double price = 0.0; + final StringBuffer sb = new StringBuffer(); + + @Override + public void onNext(Stock stock) { + count++; + price += fetchStockPriceBid(stock); + sb.append(":") + .append(stock.getTickerSymbol()); + } + + @Override + public void onCompleted() { + responseObserver.onNext( + StockQuote.newBuilder() + .setPrice(price / count) + .setDescription("Statistics:" + sb) + .build() + ); + responseObserver.onCompleted(); + } + + @Override + public void onError(Throwable t) { + logger.warning("error: " + t.getMessage()); + } + }; + } + + @Override + public StreamObserver bidirectionalStreamingGetListsStockQuotes( + StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(Stock request) { + IntStream.rangeClosed(1, 5) + .forEach(i -> { + StockQuote stockQuote = StockQuote.newBuilder() + .setPrice(fetchStockPriceBid(request)) + .setOfferNumber(i) + .setDescription("Price for stock:" + request.getTickerSymbol()) + .build(); + responseObserver.onNext(stockQuote); + }); + } + + @Override + public void onError(Throwable t) { + logger.warning("error:" + t.getMessage()); + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + + private static double fetchStockPriceBid(Stock stock) { + return stock.getTickerSymbol() + .length() + + ThreadLocalRandom.current() + .nextDouble(-0.1d, 0.1d); + } +} diff --git a/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/sample.proto b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/sample.proto new file mode 100644 index 0000000..debc893 --- /dev/null +++ b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/sample.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; +package com.github.thinkerou; +option java_multiple_files = true; + +import "google/protobuf/struct.proto"; + +message VerifyRequest { + +} + +message VerifyResponse { + google.protobuf.Struct someData = 1; +} + +service LoginService { + rpc login(VerifyRequest) returns (VerifyResponse); +} diff --git a/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/stockquotes.proto b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/stockquotes.proto new file mode 100644 index 0000000..719c2f0 --- /dev/null +++ b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/stockquotes.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; +package com.github.thinkerou; +option java_multiple_files = true; + +message StockQuote { + double price = 1; + int32 offer_number = 2; + string description = 3; +} diff --git a/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/stocks.proto b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/stocks.proto new file mode 100644 index 0000000..4e64b2d --- /dev/null +++ b/karate-grpc-core/artifacts/sample-project-for-unit-tests/test-app-protos/src/main/resources/stocks.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; +package com.github.thinkerou; +option java_multiple_files = true; + +import "stockquotes.proto"; + +service StockQuoteProvider { + + rpc unaryGetStockQuote(Stock) returns (StockQuote) {} + + rpc serverSideStreamingGetListStockQuotes(Stock) returns (stream StockQuote) {} + + rpc clientSideStreamingGetStatisticsOfStocks(stream Stock) returns (StockQuote) {} + + rpc bidirectionalStreamingGetListsStockQuotes(stream Stock) returns (stream StockQuote) {} +} +message Stock { + string ticker_symbol = 1; + string company_name = 2; + string description = 3; +} From 364b2505f1ca69f24a497575fd37222cda0f0677 Mon Sep 17 00:00:00 2001 From: Krishnan Mahadevan Date: Sun, 20 Nov 2022 22:11:09 +0530 Subject: [PATCH 2/2] Support protobin file loading from Classpath Closes #24 --- karate-grpc-core/pom.xml | 18 +++ .../thinkerou/karate/grpc/DynamicClient.java | 2 +- .../thinkerou/karate/message/Reader.java | 19 ++- .../thinkerou/karate/message/Writer.java | 2 +- .../karate/protobuf/ServiceResolver.java | 41 +++-- .../thinkerou/karate/service/GrpcCall.java | 135 +++++++++-------- .../thinkerou/karate/service/GrpcList.java | 83 ++++------ .../github/thinkerou/karate/service/Jvm.java | 56 +++++++ .../github/thinkerou/karate/service/Pair.java | 22 +++ .../thinkerou/karate/utils/DataReader.java | 98 ++++++++++++ .../thinkerou/karate/GrpcClientTest.java | 143 ++++++++++++++++++ .../thinkerou/testng/ClassPathFudger.java | 22 +++ .../thinkerou/testng/FudgeClassPath.java | 12 ++ .../thinkerou/utils/JarClassLoader.java | 38 +++++ .../com/github/thinkerou/utils/MiscUtils.java | 42 +++++ .../github/thinkerou/utils/ServerSpawner.java | 47 ++++++ .../services/org.testng.ITestNGListener | 1 + karate-grpc-proto/pom.xml | 2 +- 18 files changed, 634 insertions(+), 149 deletions(-) create mode 100644 karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/Jvm.java create mode 100644 karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/Pair.java create mode 100644 karate-grpc-core/src/main/java/com/github/thinkerou/karate/utils/DataReader.java create mode 100644 karate-grpc-core/src/test/java/com/github/thinkerou/karate/GrpcClientTest.java create mode 100644 karate-grpc-core/src/test/java/com/github/thinkerou/testng/ClassPathFudger.java create mode 100644 karate-grpc-core/src/test/java/com/github/thinkerou/testng/FudgeClassPath.java create mode 100644 karate-grpc-core/src/test/java/com/github/thinkerou/utils/JarClassLoader.java create mode 100644 karate-grpc-core/src/test/java/com/github/thinkerou/utils/MiscUtils.java create mode 100644 karate-grpc-core/src/test/java/com/github/thinkerou/utils/ServerSpawner.java create mode 100644 karate-grpc-core/src/test/resources/META-INF/services/org.testng.ITestNGListener diff --git a/karate-grpc-core/pom.xml b/karate-grpc-core/pom.xml index f237fd0..3c85eda 100644 --- a/karate-grpc-core/pom.xml +++ b/karate-grpc-core/pom.xml @@ -94,6 +94,24 @@ karate-core ${karate.version} + + + io.github.classgraph + classgraph + 4.8.132 + + + org.testng + testng + 7.5 + test + + + org.assertj + assertj-core + 3.23.1 + test + diff --git a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/grpc/DynamicClient.java b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/grpc/DynamicClient.java index d2be8b1..02cf6dc 100644 --- a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/grpc/DynamicClient.java +++ b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/grpc/DynamicClient.java @@ -170,7 +170,7 @@ private String getFullMethodName() { /** * Returns the appropriate method type based on whether the client or server expect streams. */ - private MethodDescriptor.MethodType getMethodType() { + public MethodDescriptor.MethodType getMethodType() { boolean clientStreaming = protoMethodDescriptor.toProto().getClientStreaming(); boolean serverStreaming = protoMethodDescriptor.toProto().getServerStreaming(); diff --git a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/message/Reader.java b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/message/Reader.java index e0a1daf..aed9836 100644 --- a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/message/Reader.java +++ b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/message/Reader.java @@ -1,5 +1,6 @@ package com.github.thinkerou.karate.message; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.logging.Logger; @@ -13,7 +14,6 @@ /** * Reader - * * A utility class which knows how to read proto files written using message.Writer. * * @author thinkerou @@ -26,6 +26,12 @@ public final class Reader { private final Descriptors.Descriptor descriptor; private final List> payloadList; + Reader(JsonFormat.Parser jsonParser, + Descriptors.Descriptor descriptor, + Map payload) { + this(jsonParser, descriptor, Collections.singletonList(payload)); + } + /** * @param jsonParser json parser * @param descriptor descriptor @@ -45,11 +51,18 @@ public final class Reader { * @param registry registry * @return Reader */ - public static Reader create(Descriptors.Descriptor descriptor, List> payloadList, - JsonFormat.TypeRegistry registry) { + public static Reader create(Descriptors.Descriptor descriptor, + List> payloadList, + JsonFormat.TypeRegistry registry) { return new Reader(JsonFormat.parser().usingTypeRegistry(registry), descriptor, payloadList); } + public static Reader create(Descriptors.Descriptor descriptor, + Map payload, + JsonFormat.TypeRegistry registry) { + return new Reader(JsonFormat.parser().usingTypeRegistry(registry), descriptor, payload); + } + /** * @return ImmutableList */ diff --git a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/message/Writer.java b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/message/Writer.java index 050d8bf..a1025e9 100644 --- a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/message/Writer.java +++ b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/message/Writer.java @@ -13,7 +13,7 @@ * Writer * * A StreamObserver which writes the contents of the received messages to an Output. - * The messages are writting in a newline-separated json format. + * The messages are written in a newline-separated json format. * * @author thinkerou */ diff --git a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/protobuf/ServiceResolver.java b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/protobuf/ServiceResolver.java index 297bc2d..223b788 100644 --- a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/protobuf/ServiceResolver.java +++ b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/protobuf/ServiceResolver.java @@ -1,9 +1,12 @@ package com.github.thinkerou.karate.protobuf; +import com.google.protobuf.Descriptors.MethodDescriptor; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.logging.Logger; import com.google.common.collect.ImmutableList; @@ -50,7 +53,7 @@ public static ServiceResolver fromFileDescriptorSet(DescriptorProtos.FileDescrip } /** - * Lists all of the services found in the file descriptors. + * Lists all the services found in the file descriptors. * * @return Iterable */ @@ -84,38 +87,32 @@ private ServiceResolver(Iterable fileDescriptors) { * @param method method * @return MethodDescriptor */ - public Descriptors.MethodDescriptor resolveServiceMethod(ProtoName method) { + public Optional resolveServiceMethod(ProtoName method) { return resolveServiceMethod( method.getServiceName(), method.getMethodName(), method.getPackageName()); } - private Descriptors.MethodDescriptor resolveServiceMethod( + private Optional resolveServiceMethod( String serviceName, String methodName, String packageName) { - Descriptors.ServiceDescriptor service = findService(serviceName, packageName); - Descriptors.MethodDescriptor method = service.findMethodByName(methodName); - if (method == null) { - throw new IllegalArgumentException( - "Can't find method " + methodName + " in service " + serviceName); - } - - return method; + return findService(serviceName, packageName) + .map(serviceDescriptor -> serviceDescriptor.findMethodByName(methodName)); } - private Descriptors.ServiceDescriptor findService(String serviceName, String packageName) { - for (Descriptors.FileDescriptor fileDescriptor : fileDescriptors) { - if (!fileDescriptor.getPackage().equals(packageName)) { - // Package does not match this file, ignore. - continue; - } + private Optional findService(String serviceName, String packageName) { + return getFileDescriptors().stream() + .filter(each -> each.getPackage().equals(packageName)) + .map(each -> each.findServiceByName(serviceName)) + .filter(Objects::nonNull) + .findFirst(); + } - Descriptors.ServiceDescriptor serviceDescriptor = fileDescriptor.findServiceByName(serviceName); - if (serviceDescriptor != null) { - return serviceDescriptor; - } + private synchronized ImmutableList getFileDescriptors() { + if (fileDescriptors.isEmpty()) { + listServices(); } - throw new IllegalArgumentException("Can't find service with name: " + serviceName); + return fileDescriptors; } /** diff --git a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/GrpcCall.java b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/GrpcCall.java index c2c5362..4d4717d 100644 --- a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/GrpcCall.java +++ b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/GrpcCall.java @@ -1,37 +1,39 @@ package com.github.thinkerou.karate.service; -import static com.google.protobuf.DescriptorProtos.FileDescriptorSet; import static com.google.protobuf.util.JsonFormat.TypeRegistry; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Logger; - -import com.github.thinkerou.karate.constants.DescriptorFile; import com.github.thinkerou.karate.domain.ProtoName; import com.github.thinkerou.karate.grpc.ChannelFactory; import com.github.thinkerou.karate.grpc.ComponentObserver; import com.github.thinkerou.karate.grpc.DynamicClient; import com.github.thinkerou.karate.message.Reader; +import com.github.thinkerou.karate.message.Writer; import com.github.thinkerou.karate.protobuf.ProtoFullName; import com.github.thinkerou.karate.protobuf.ServiceResolver; +import com.github.thinkerou.karate.utils.DataReader; import com.github.thinkerou.karate.utils.FileHelper; import com.github.thinkerou.karate.utils.RedisHelper; import com.google.common.collect.ImmutableList; +import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import com.google.protobuf.Descriptors; +import com.google.protobuf.Descriptors.MethodDescriptor; import com.google.protobuf.DynamicMessage; -import com.github.thinkerou.karate.message.Writer; - import io.grpc.CallOptions; import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor.MethodType; import io.grpc.stub.StreamObserver; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Logger; /** * GrpcCall @@ -89,64 +91,48 @@ public String invokeByRedis(String name, String payload, RedisHelper redisHelper */ private String execute(String name, String payload, RedisHelper redisHelper) { ProtoName protoName = ProtoFullName.parse(name); - byte[] data; - if (redisHelper != null) { - data = redisHelper.getDescriptorSets(); - } else { - String path = System.getProperty("user.home") + DescriptorFile.PROTO_PATH.getText(); - Path descriptorPath = Paths.get(path + DescriptorFile.PROTO_FILE.getText()); - FileHelper.validatePath(Optional.ofNullable(descriptorPath)); - try { - data = Files.readAllBytes(descriptorPath); - } catch (IOException e) { - throw new IllegalArgumentException("Read descriptor path failed: " + descriptorPath.toString()); - } - } - // Fetch the appropriate file descriptors for the service. - FileDescriptorSet fileDescriptorSet; - try { - fileDescriptorSet = FileDescriptorSet.parseFrom(data); - } catch (IOException e) { - throw new IllegalArgumentException("File descriptor set parse fail: " + e.getMessage()); - } + Pair> found = + DataReader.read(redisHelper) + .stream() + .map(each -> new Pair<>(each, each.resolveServiceMethod(protoName))) + .filter(each -> each.right().isPresent()) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Service Resolver Not found")); + + Descriptors.MethodDescriptor methodDescriptor = found.right() + .orElseThrow(() -> { + // When can't find service or method with name + // use service or/and method search once for help user + GrpcList list = new GrpcList(); + String result1 = list.invoke(protoName.getServiceName(), "", false); + String result2 = list.invoke("", protoName.getMethodName(), false); + if (!result1.equals("[]") || !result2.equals("[{}]")) { + logger.warning( + "Call grpc failed, maybe you should see the follow grpc information."); + if (!result1.equals("[]")) { + logger.info(result1); + } + if (!result2.equals("[{}]")) { + logger.info(result2); + } + } + String text = "Can't find method " + protoName.getMethodName() + + " in service " + protoName.getServiceName(); + return new IllegalArgumentException(text); + }); // Set up the dynamic client and make the call. - ServiceResolver serviceResolver = ServiceResolver.fromFileDescriptorSet(fileDescriptorSet); - Descriptors.MethodDescriptor methodDescriptor; - try { - methodDescriptor = serviceResolver.resolveServiceMethod(protoName); - } catch (IllegalArgumentException e) { - // When can't find service or method with name - // use service or/and method search once for help user - GrpcList list = new GrpcList(); - String result1 = list.invoke(protoName.getServiceName(), "", false); - String result2 = list.invoke("", protoName.getMethodName(), false); - if (!result1.equals("[]") || !result2.equals("[{}]")) { - logger.warning("Call grpc failed, maybe you should see the follow grpc information."); - if (!result1.equals("[]")) { - logger.info(result1); - } - if (!result2.equals("[{}]")) { - logger.info(result2); - } - } - throw new IllegalArgumentException(e.getMessage()); - } // Create a dynamic grpc client. DynamicClient dynamicClient = DynamicClient.create(methodDescriptor, channel); // This collects all know types into a registry for resolution of potential "Any" types. - TypeRegistry registry = TypeRegistry.newBuilder().add(serviceResolver.listMessageTypes()).build(); - - // Convert payload string to list. - List> payloadList = new Gson().fromJson(payload, List.class); + TypeRegistry registry = TypeRegistry.newBuilder().add(found.left().listMessageTypes()).build(); // Need to support stream so it's a list. - final ImmutableList requestMessages = Reader.create( - methodDescriptor.getInputType(), payloadList, registry - ).read(); + final ImmutableList requestMessages = + extractRequestMessages(payload, methodDescriptor, registry); // Creates one temp file to save call grpc result. Path filePath = null; @@ -156,17 +142,21 @@ private String execute(String name, String payload, RedisHelper redisHelper) { logger.warning(e.getMessage()); } FileHelper.validatePath(Optional.ofNullable(filePath)); - + StreamObserver streamObserver; List output = new ArrayList<>(); - StreamObserver streamObserver = ComponentObserver.of(Writer.create(output, registry)); + streamObserver = ComponentObserver.of(Writer.create(output, registry)); // Calls grpc! try { - dynamicClient.call(requestMessages, streamObserver, callOptions()).get(); + Objects.requireNonNull( + dynamicClient.call(requestMessages, streamObserver, callOptions())).get(); } catch (Throwable t) { throw new RuntimeException("Caught exception while waiting for rpc", t); } - + if (dynamicClient.getMethodType() == MethodType.UNARY || + dynamicClient.getMethodType() == MethodType.CLIENT_STREAMING) { + return output.get(0).toString(); + } return output.toString(); } @@ -176,4 +166,17 @@ private static CallOptions callOptions() { return result; } + private static ImmutableList extractRequestMessages(String raw, + Descriptors.MethodDescriptor methodDescriptor, TypeRegistry registry) { + Type type = new TypeToken>>() {}.getType(); + try { + List> payloads = new Gson().fromJson(raw, type); + return Reader.create(methodDescriptor.getInputType(), payloads, registry).read(); + } catch (JsonSyntaxException ignored) { + type = new TypeToken>() {}.getType(); + Map payload = new Gson().fromJson(raw, type); + return Reader.create(methodDescriptor.getInputType(), payload, registry).read(); + } + } + } diff --git a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/GrpcList.java b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/GrpcList.java index 901e758..d81fd1a 100644 --- a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/GrpcList.java +++ b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/GrpcList.java @@ -1,28 +1,19 @@ package com.github.thinkerou.karate.service; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import com.github.thinkerou.karate.constants.DescriptorFile; import com.github.thinkerou.karate.domain.ProtoName; import com.github.thinkerou.karate.protobuf.ProtoFullName; -import com.github.thinkerou.karate.protobuf.ServiceResolver; -import com.github.thinkerou.karate.utils.FileHelper; +import com.github.thinkerou.karate.utils.DataReader; import com.github.thinkerou.karate.utils.RedisHelper; import com.google.gson.Gson; -import com.google.protobuf.DescriptorProtos; import com.google.protobuf.Descriptors; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; /** * GrpcList - * * Utility to list the services, methods and message definitions for the known grpc end-points. * * @author thinkerou @@ -87,50 +78,33 @@ private List> execute(String name, Boolean withMessage, Redi /** * List the grpc services filtered by service name (contains) or method name (contains). - * * Mainly goal: return value are used web page. */ - private List> execute(String serviceFilter, String methodFilter, Boolean withMessage, - Boolean saveOutput, RedisHelper redisHelper) { - byte[] data; - if (redisHelper != null) { - data = redisHelper.getDescriptorSets(); - } else { - String path = System.getProperty("user.home") + DescriptorFile.PROTO_PATH.getText(); - Path descriptorPath = Paths.get(path + DescriptorFile.PROTO_FILE.getText()); - FileHelper.validatePath(Optional.ofNullable(descriptorPath)); - try { - data = Files.readAllBytes(descriptorPath); - } catch (IOException e) { - throw new IllegalArgumentException("Read descriptor fail: " + descriptorPath.toString()); - } - } - - // Fetch the appropriate file descriptors for the service. - DescriptorProtos.FileDescriptorSet fileDescriptorSet; - try { - fileDescriptorSet = DescriptorProtos.FileDescriptorSet.parseFrom(data); - } catch (IOException e) { - throw new IllegalArgumentException("Descriptor file parse fail: " + e.getMessage()); - } - - ServiceResolver serviceResolver = ServiceResolver.fromFileDescriptorSet(fileDescriptorSet); - - Iterable serviceDescriptorIterable = serviceResolver.listServices(); + private List> execute( + String serviceFilter, + String methodFilter, + Boolean withMessage, + Boolean saveOutput, + RedisHelper redisHelper) { List> output = new ArrayList<>(); - serviceDescriptorIterable.forEach(descriptor -> { - if (serviceFilter.isEmpty() - || descriptor.getFullName().toLowerCase().contains(serviceFilter.toLowerCase())) { - Map result = new HashMap<>(); - listMethods(result, descriptor, methodFilter, withMessage, saveOutput); - - if (!result.isEmpty()) { - output.add(result); - } - } - }); + DataReader.read(redisHelper) + .stream() + .flatMap(each -> StreamSupport.stream(each.listServices().spliterator(), false)) + .forEach( + descriptor -> { + if (serviceFilter.isEmpty() + || descriptor.getFullName().toLowerCase() + .contains(serviceFilter.toLowerCase())) { + Map result = new HashMap<>(); + listMethods(result, descriptor, methodFilter, withMessage, saveOutput); + + if (!result.isEmpty()) { + output.add(result); + } + } + }); return output; } @@ -202,5 +176,4 @@ private static Object renderFieldDescriptor(Descriptors.FieldDescriptor descript return descriptor.getJavaType(); } } - } diff --git a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/Jvm.java b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/Jvm.java new file mode 100644 index 0000000..c0d8232 --- /dev/null +++ b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/Jvm.java @@ -0,0 +1,56 @@ +package com.github.thinkerou.karate.service; + +import com.github.thinkerou.karate.constants.DescriptorFile; +import com.google.common.base.Preconditions; +import java.io.File; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public final class Jvm { + + public static final String PROTOBIN_DIR = "protobin.dir"; + + private Jvm() { + //defeat instantiation + } + + public static boolean isProtoBinDirSpecifiedViaJVMArguments() { + return !System.getProperty(PROTOBIN_DIR, "").trim().isEmpty(); + } + + public static List getProtoBinFiles() { + String location = System.getProperty(PROTOBIN_DIR); + String errorMsg = String.format("Please specific the directory that contains the " + + "protobuf binary file(s) via the JVM argument -D%s=", PROTOBIN_DIR); + Preconditions.checkNotNull(location, errorMsg); + Preconditions.checkArgument(!location.isEmpty(), errorMsg); + File directory = Paths.get(location).toFile(); + Preconditions.checkArgument(directory.isDirectory(), errorMsg); + File[] files = Optional.ofNullable( + directory.listFiles(file -> file.getName().toLowerCase().endsWith(".protobin"))) + .orElse(new File[0]); + errorMsg = "No protobin files (*.protobin) found in directory " + location; + Preconditions.checkArgument(files.length != 0, errorMsg); + List result = Arrays.stream(files) + .filter(each -> !each.isDirectory()) + .map(File::getAbsolutePath) + .collect(Collectors.toCollection(ArrayList::new)); + defaultProtobufFile().ifPresent(result::add); + return result; + } + + private static Optional defaultProtobufFile() { + String path = System.getProperty("user.home") + DescriptorFile.PROTO_PATH.getText(); + File file = Paths.get(path + DescriptorFile.PROTO_FILE.getText()).toFile(); + if (file.isFile() && file.exists()) { + return Optional.of(file.getAbsolutePath()); + } + return Optional.empty(); + } + + +} diff --git a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/Pair.java b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/Pair.java new file mode 100644 index 0000000..577289a --- /dev/null +++ b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/service/Pair.java @@ -0,0 +1,22 @@ +package com.github.thinkerou.karate.service; + +import java.util.Objects; + +public final class Pair { + + private final L left; + private final R right; + + public Pair(L left, R right) { + this.left = Objects.requireNonNull(left, "Value cannot be null"); + this.right = Objects.requireNonNull(right, "Value cannot be null"); + } + + public L left() { + return left; + } + + public R right() { + return right; + } +} diff --git a/karate-grpc-core/src/main/java/com/github/thinkerou/karate/utils/DataReader.java b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/utils/DataReader.java new file mode 100644 index 0000000..1338b1e --- /dev/null +++ b/karate-grpc-core/src/main/java/com/github/thinkerou/karate/utils/DataReader.java @@ -0,0 +1,98 @@ +package com.github.thinkerou.karate.utils; + +import com.github.thinkerou.karate.protobuf.ServiceResolver; +import com.github.thinkerou.karate.service.Jvm; +import com.google.common.io.ByteStreams; +import com.google.protobuf.DescriptorProtos; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.Resource; +import io.github.classgraph.ScanResult; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public final class DataReader { + + private static final Logger log = Logger.getLogger(DataReader.class.getName()); + + private static volatile List resolverCache; + + private DataReader() { + } + + public static List read(RedisHelper redisHelper) { + if (resolverCache != null) { + return resolverCache; + } + resolverCache = _read(redisHelper); + return resolverCache; + } + + private static synchronized List _read(RedisHelper redisHelper) { + List data; + if (redisHelper != null) { + data = Collections.singletonList(redisHelper.getDescriptorSets()); + } else { + data = mapper().values() + .stream() + .map(each -> { + try { + return ByteStreams.toByteArray(each); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + } + + List resolvers = new ArrayList<>(); + for (byte[] datum: data) { + // Fetch the appropriate file descriptors for the service. + DescriptorProtos.FileDescriptorSet fileDescriptorSet; + try { + fileDescriptorSet = DescriptorProtos.FileDescriptorSet.parseFrom(datum); + } catch (IOException e) { + throw new IllegalArgumentException("Descriptor file parse fail: " + e.getMessage()); + } + resolvers.add(ServiceResolver.fromFileDescriptorSet(fileDescriptorSet)); + } + return resolvers; + } + + private static Map mapper() { + Map mapping = new HashMap<>(); + if (Jvm.isProtoBinDirSpecifiedViaJVMArguments()) { + Function asStream = fileName -> { + try { + return new FileInputStream(fileName); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + }; + Jvm.getProtoBinFiles() + .forEach(file -> mapping.put(file, asStream.apply(file))); + } + else { + log.info("Will search through the CLASSPATH for *.protobin files."); + try (ScanResult scanResult = new ClassGraph().scan()) { + scanResult.getResourcesWithExtension("protobin") + .forEachByteArrayIgnoringIOException( + (Resource res, byte[] content) -> + mapping.put(res.getPath(), new ByteArrayInputStream(content)) + ); + } + } + return mapping; + } + +} diff --git a/karate-grpc-core/src/test/java/com/github/thinkerou/karate/GrpcClientTest.java b/karate-grpc-core/src/test/java/com/github/thinkerou/karate/GrpcClientTest.java new file mode 100644 index 0000000..92ad761 --- /dev/null +++ b/karate-grpc-core/src/test/java/com/github/thinkerou/karate/GrpcClientTest.java @@ -0,0 +1,143 @@ +package com.github.thinkerou.karate; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.thinkerou.karate.service.Jvm; +import com.github.thinkerou.karate.service.Pair; +import com.github.thinkerou.testng.FudgeClassPath; +import com.github.thinkerou.utils.MiscUtils; +import com.github.thinkerou.utils.ServerSpawner; +import com.google.gson.JsonArray; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; +import java.util.stream.IntStream; +import org.assertj.core.api.AssertionsForClassTypes; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class GrpcClientTest { + private static final int PORT = 8080; + private ServerSpawner spawner; + private static final String baseDir = System.getProperty("user.dir") + "/artifacts/"; + private static final String protoBinDir = baseDir + "/protobins"; + private static final String payload = "{\"ticker_symbol\":\"MSFT\",\"company_name\":\"Microsoft Corp\",\"description\":\"Microsoft company\"}"; + + private static final Logger log = Logger.getLogger(GrpcClientTest.class.getName()); + + @BeforeClass + public void setup() throws IOException, InterruptedException { + spawner = new ServerSpawner(); + spawner.start(); + TimeUnit.SECONDS.sleep(5); + log.info("GRPC Server application has been started"); + } + + @Test + public void testUnaryMethodWithProtoBinFromJVMArguments() { + try { + String location = new File(protoBinDir).getAbsolutePath(); + System.setProperty(Jvm.PROTOBIN_DIR, location); + runUnaryTest(); + } finally { + System.setProperty(Jvm.PROTOBIN_DIR, ""); + } + } + + @Test(dependsOnMethods = "testUnaryMethodWithProtoBinFromJVMArguments") + @FudgeClassPath + public void testUnaryMethodWithProtoBinFromResources() { + runUnaryTest(); + } + + @Test(dependsOnMethods = "testUnaryMethodWithProtoBinFromJVMArguments") + @FudgeClassPath + public void testClientStreamingRequests() { + GrpcClient client = new GrpcClient("localhost", PORT); + String payload = "[" + + "{\"ticker_symbol\":\"MSFT\",\"company_name\":\"Microsoft Corp\",\"description\":\"Microsoft company\"}" + + "," + + "{\"ticker_symbol\":\"AAPL\",\"company_name\":\"Apple Inc\",\"description\":\"Apple company\"}" + + "," + + "{\"ticker_symbol\":\"TSLA\",\"company_name\":\"Tesla Inc\",\"description\":\"Tesla company\"}" + + "]"; + String raw = client.call( + "com.github.thinkerou.StockQuoteProvider/clientSideStreamingGetStatisticsOfStocks", + payload); + String description = MiscUtils.extractDescription(raw); + AssertionsForClassTypes.assertThat(description) + .withFailMessage("Should have got a proper response") + .isEqualTo("Statistics::MSFT:AAPL:TSLA"); + } + + @Test(dependsOnMethods = "testUnaryMethodWithProtoBinFromJVMArguments") + @FudgeClassPath + public void testServerStreamingRequests() { + GrpcClient client = new GrpcClient("localhost", PORT); + String payload = + "{\"ticker_symbol\":\"MSFT\",\"company_name\":\"Microsoft Corp\",\"description\":\"Microsoft company\"}"; + String raw = client.call( + "com.github.thinkerou.StockQuoteProvider/serverSideStreamingGetListStockQuotes", + payload); + JsonArray array = MiscUtils.asArray(raw); + runAssertionsOnArray(array, "Price for stock:MSFT"); + } + + @Test + @FudgeClassPath + public void testBidirectionalStreaming() { + GrpcClient client = new GrpcClient("localhost", PORT); + String payload = "[" + + "{\"ticker_symbol\":\"AAPL\",\"company_name\":\"Apple Inc\",\"description\":\"Apple company\"}" + + "," + + "{\"ticker_symbol\":\"TSLA\",\"company_name\":\"Tesla Inc\",\"description\":\"Tesla company\"}" + + "]"; + String raw = client.call( + "com.github.thinkerou.StockQuoteProvider/bidirectionalStreamingGetListsStockQuotes", + payload); + JsonArray array = MiscUtils.asArray(raw); + AssertionsForClassTypes.assertThat(array.size()) + .withFailMessage("We should have got 10 elements in the response") + .isEqualTo(10); + JsonArray apple = MiscUtils.filter(array, new Pair<>("description", "Price for stock:AAPL")); + runAssertionsOnArray(apple, "Price for stock:AAPL"); + JsonArray tesla = MiscUtils.filter(array, new Pair<>("description", "Price for stock:TSLA")); + runAssertionsOnArray(tesla, "Price for stock:TSLA"); + } + + private static void runAssertionsOnArray(JsonArray array, String descriptionToValidate) { + final int five = 5; + AssertionsForClassTypes.assertThat(array.size()) + .withFailMessage("We should have got " + five + " elements in the response") + .isEqualTo(five); + AtomicInteger counter = new AtomicInteger(0); + IntStream.rangeClosed(0, five - 1) + .mapToObj(array::get) + .map(each -> each.getAsJsonObject().toString()) + .forEach(each -> { + AssertionsForClassTypes.assertThat(MiscUtils.extractDescription(each)) + .withFailMessage("We should have got a valid description") + .isEqualTo(descriptionToValidate); + AssertionsForClassTypes.assertThat(MiscUtils.extractOfferNumber(each)) + .withFailMessage("We should have got a valid offer number") + .isEqualTo(counter.incrementAndGet()); + }); + } + @AfterClass + public void cleanUp() throws IOException, InterruptedException { + spawner.stop(); + log.info("GRPC Server application has been stopped"); + } + + private static void runUnaryTest() { + GrpcClient client = new GrpcClient("localhost", PORT); + String raw = client.call("com.github.thinkerou.StockQuoteProvider/unaryGetStockQuote", payload); + String description = MiscUtils.extractDescription(raw); + assertThat(description) + .withFailMessage("Should have got a proper response") + .isEqualTo("Price for stock:MSFT"); + } +} diff --git a/karate-grpc-core/src/test/java/com/github/thinkerou/testng/ClassPathFudger.java b/karate-grpc-core/src/test/java/com/github/thinkerou/testng/ClassPathFudger.java new file mode 100644 index 0000000..ee827f0 --- /dev/null +++ b/karate-grpc-core/src/test/java/com/github/thinkerou/testng/ClassPathFudger.java @@ -0,0 +1,22 @@ +package com.github.thinkerou.testng; + +import com.github.thinkerou.utils.JarClassLoader; +import org.testng.IInvokedMethod; +import org.testng.IInvokedMethodListener; +import org.testng.ITestResult; + +public class ClassPathFudger implements IInvokedMethodListener { + + @Override + public void beforeInvocation(IInvokedMethod method, ITestResult testResult) { + if (method.isConfigurationMethod()) { + return; + } + FudgeClassPath alter = method.getTestMethod().getConstructorOrMethod() + .getMethod().getAnnotation(FudgeClassPath.class); + if (alter == null) { + return; + } + JarClassLoader.loadLibrary(); + } +} diff --git a/karate-grpc-core/src/test/java/com/github/thinkerou/testng/FudgeClassPath.java b/karate-grpc-core/src/test/java/com/github/thinkerou/testng/FudgeClassPath.java new file mode 100644 index 0000000..1df90a4 --- /dev/null +++ b/karate-grpc-core/src/test/java/com/github/thinkerou/testng/FudgeClassPath.java @@ -0,0 +1,12 @@ +package com.github.thinkerou.testng; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(java.lang.annotation.RetentionPolicy.RUNTIME) +@Target(java.lang.annotation.ElementType.METHOD) +@Documented +public @interface FudgeClassPath { + +} diff --git a/karate-grpc-core/src/test/java/com/github/thinkerou/utils/JarClassLoader.java b/karate-grpc-core/src/test/java/com/github/thinkerou/utils/JarClassLoader.java new file mode 100644 index 0000000..04523a3 --- /dev/null +++ b/karate-grpc-core/src/test/java/com/github/thinkerou/utils/JarClassLoader.java @@ -0,0 +1,38 @@ +package com.github.thinkerou.utils; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.logging.Logger; + +public class JarClassLoader extends URLClassLoader { + + private static final Logger log = Logger.getLogger(JarClassLoader.class.getName()); + private final URL url; + + private JarClassLoader(URL url, ClassLoader parent) { + super(new URL[]{url}, parent); + this.url = url; + } + + private void load() { + addURL(url); + } + + public static void loadLibrary() { + File jar = new File(System.getProperty("user.dir") + "/artifacts/jars/server.jar"); + log.warning("Adding the jar " + jar.getAbsolutePath() + " to the classpath"); + try { + URL url = jar.toURI().toURL(); + + ClassLoader loader = ClassLoader.getSystemClassLoader(); + JarClassLoader jarClassLoader = new JarClassLoader(url, loader); + Thread.currentThread().setContextClassLoader(jarClassLoader); + jarClassLoader.load(); + } catch (Exception ex) { + throw new RuntimeException( + "Cannot load library from jar file '" + jar.getAbsolutePath() + "'. Reason: " + + ex.getMessage(), ex); + } + } +} diff --git a/karate-grpc-core/src/test/java/com/github/thinkerou/utils/MiscUtils.java b/karate-grpc-core/src/test/java/com/github/thinkerou/utils/MiscUtils.java new file mode 100644 index 0000000..ca172fd --- /dev/null +++ b/karate-grpc-core/src/test/java/com/github/thinkerou/utils/MiscUtils.java @@ -0,0 +1,42 @@ +package com.github.thinkerou.utils; + +import com.github.thinkerou.karate.service.Pair; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import java.io.StringReader; + +public class MiscUtils { + + public static String extractMessage(String raw) { + return extract(raw, "message"); + } + + public static String extractDescription(String raw) { + return extract(raw, "description"); + } + + private static String extract(String raw, String key) { + JsonElement response = new JsonParser().parse(new StringReader(raw)); + return response.getAsJsonObject().get(key).getAsString(); + } + + public static int extractOfferNumber(String raw) { + JsonElement response = new JsonParser().parse(new StringReader(raw)); + return response.getAsJsonObject().get("offer_number").getAsInt(); + } + + public static JsonArray asArray(String raw) { + return new JsonParser().parse(new StringReader(raw)).getAsJsonArray(); + } + + public static JsonArray filter(JsonArray array, Pair kvp) { + JsonArray result = new JsonArray(); + for (int i = 0; i < array.size(); i++) { + if (array.get(i).getAsJsonObject().get(kvp.left()).getAsString().equals(kvp.right())) { + result.add(array.get(i)); + } + } + return result; + } +} diff --git a/karate-grpc-core/src/test/java/com/github/thinkerou/utils/ServerSpawner.java b/karate-grpc-core/src/test/java/com/github/thinkerou/utils/ServerSpawner.java new file mode 100644 index 0000000..1a86389 --- /dev/null +++ b/karate-grpc-core/src/test/java/com/github/thinkerou/utils/ServerSpawner.java @@ -0,0 +1,47 @@ +package com.github.thinkerou.utils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +public class ServerSpawner { + + private Process process; + private final File logFile = new File(System.getProperty("user.dir") + "/target/server.log"); + private static final Logger log = Logger.getLogger(ServerSpawner.class.getName()); + + //To add new tests that involve invoking the server.jar the following needs to be done + //1. Add the changes to the test project artifacts/sample-project-for-unit-tests/test-app-protos + //2. Build the project to generate the "server.jar" + //3. Copy the jar "artifacts/sample-project-for-unit-tests/test-app-protos/target/server.jar" to + // "artifacts/jars" + //4. Copy the protobin file "artifacts/sample-project-for-unit-tests/test-app-protos/target/classes/test-app-protos.protobin" to + // "artifacts/protobins" + + public void start() throws IOException { + List commands = Arrays.asList( + "java", + "-jar", + "artifacts/jars/server.jar" + ); + process = new ProcessBuilder(commands) + .redirectErrorStream(true) + .redirectError(logFile) + .start(); + } + + public void stop() throws IOException, InterruptedException { + process = process.destroyForcibly(); + process.waitFor(); + if (logFile.exists()) { + List lines = Files.readAllLines(Paths.get(logFile.getAbsolutePath())); + if (!lines.isEmpty()) { + log.info("Errors : " + lines); + } + } + } +} diff --git a/karate-grpc-core/src/test/resources/META-INF/services/org.testng.ITestNGListener b/karate-grpc-core/src/test/resources/META-INF/services/org.testng.ITestNGListener new file mode 100644 index 0000000..57f9c53 --- /dev/null +++ b/karate-grpc-core/src/test/resources/META-INF/services/org.testng.ITestNGListener @@ -0,0 +1 @@ +com.github.thinkerou.testng.ClassPathFudger diff --git a/karate-grpc-proto/pom.xml b/karate-grpc-proto/pom.xml index fb85d15..0e9f289 100644 --- a/karate-grpc-proto/pom.xml +++ b/karate-grpc-proto/pom.xml @@ -60,4 +60,4 @@ - \ No newline at end of file +