diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 8bbec602..51ead043 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -17,7 +17,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom + extensions: dom, sockets, grpc, curl - name: Check Out Code uses: actions/checkout@v2 @@ -65,7 +65,7 @@ jobs: with: php-version: ${{ matrix.php }} tools: composer:v2 - extensions: dom, sockets, curl + extensions: dom, sockets, grpc, curl - name: Check Out Code uses: actions/checkout@v2 @@ -118,7 +118,7 @@ jobs: with: php-version: ${{ matrix.php }} tools: composer:v2 - extensions: dom, sockets, curl + extensions: dom, sockets, grpc, curl - name: Check Out Code uses: actions/checkout@v2 diff --git a/composer.json b/composer.json index c356d757..26bd84d6 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,8 @@ }, "autoload": { "psr-4": { + "Temporal\\Api\\Testservice\\": "testing/api/testservice/Temporal/Api/Testservice", + "GPBMetadata\\Temporal\\Api\\Testservice\\": "testing/api/testservice/GPBMetadata/Temporal/Api/Testservice", "Temporal\\Testing\\": "testing/src", "GPBMetadata\\": "api/v1/GPBMetadata", "Temporal\\": "src", diff --git a/testing/Readme.md b/testing/Readme.md index c5c4e113..85bac9cb 100644 --- a/testing/Readme.md +++ b/testing/Readme.md @@ -63,6 +63,35 @@ $worker->registerActivity(MyActvivityMock::class); $factory->run(); ``` +### Time management +By default, the test server starts with `--enable-time-skipping` option. It means that if the +workflow has a timer, the server doesn't wait for it and continues immediately. To change +this behaviour you can use `TestService` class: + +```php +$testService = TestService::create('localhost:7233'); +$testService->lockTimeSkipping(); + +// ... +$testService->unlockTimeSkipping(); +``` + +Class `TestService` communicates with a test server and provides method for "time management". Time skipping +can be switched on/off with `unlockTimeSkipping()` and `lockTimeSkipping()` method. + +In case you need to emulate some "waiting" on a test server, you can use `sleep(int secods)` or `sleepUntil(int $timestamp)` methods. + +Current server time can be retrieved with `getCurrentTime(): Carbon` method. + +For convenience if you don't want to skip time in the whole `TestCase` class use `WithoutTimeSkipping`: + +```php +final class MyWorkflowTest extends TestCase +{ + use WithoutTimeSkipping; +} +``` + diff --git a/testing/api/testservice/GPBMetadata/Dependencies/Gogoproto/Gogo.php b/testing/api/testservice/GPBMetadata/Dependencies/Gogoproto/Gogo.php new file mode 100644 index 00000000..86c1bb14 --- /dev/null +++ b/testing/api/testservice/GPBMetadata/Dependencies/Gogoproto/Gogo.php @@ -0,0 +1,26 @@ +internalAddGeneratedFile( + ' +~ +!dependencies/gogoproto/gogo.proto gogoproto google/protobuf/descriptor.protoB$Z"github.com/gogo/protobuf/gogoprotobproto3' + , true); + + static::$is_initialized = true; + } +} + diff --git a/testing/api/testservice/GPBMetadata/Temporal/Api/Testservice/V1/RequestResponse.php b/testing/api/testservice/GPBMetadata/Temporal/Api/Testservice/V1/RequestResponse.php new file mode 100644 index 00000000..4a9306b9 --- /dev/null +++ b/testing/api/testservice/GPBMetadata/Temporal/Api/Testservice/V1/RequestResponse.php @@ -0,0 +1,41 @@ +internalAddGeneratedFile( + ' + +2temporal/api/testservice/v1/request_response.prototemporal.api.testservice.v1google/protobuf/timestamp.proto!dependencies/gogoproto/gogo.proto" +LockTimeSkippingRequest" +LockTimeSkippingResponse" +UnlockTimeSkippingRequest" +UnlockTimeSkippingResponse"H +SleepUntilRequest3 + timestamp ( 2.google.protobuf.TimestampB"A + SleepRequest1 +duration ( 2.google.protobuf.DurationB" + SleepResponse"H +GetCurrentTimeResponse. +time ( 2.google.protobuf.TimestampBB +io.temporal.api.testservice.v1BRequestResponseProtoPZ-go.temporal.io/api/testservice/v1;testserviceTemporal.Api.TestService.V1Temporal::Api::TestService::V1bproto3' + , true); + + static::$is_initialized = true; + } +} + diff --git a/testing/api/testservice/GPBMetadata/Temporal/Api/Testservice/V1/Service.php b/testing/api/testservice/GPBMetadata/Temporal/Api/Testservice/V1/Service.php new file mode 100644 index 00000000..d90c94b7 Binary files /dev/null and b/testing/api/testservice/GPBMetadata/Temporal/Api/Testservice/V1/Service.php differ diff --git a/testing/api/testservice/Temporal/Api/Testservice/V1/GetCurrentTimeResponse.php b/testing/api/testservice/Temporal/Api/Testservice/V1/GetCurrentTimeResponse.php new file mode 100644 index 00000000..3f6b2b46 --- /dev/null +++ b/testing/api/testservice/Temporal/Api/Testservice/V1/GetCurrentTimeResponse.php @@ -0,0 +1,68 @@ +temporal.api.testservice.v1.GetCurrentTimeResponse + */ +class GetCurrentTimeResponse extends \Google\Protobuf\Internal\Message +{ + /** + * Generated from protobuf field .google.protobuf.Timestamp time = 1 [(.gogoproto.stdtime) = true]; + */ + protected $time = null; + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * @type \Google\Protobuf\Timestamp $time + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Temporal\Api\Testservice\V1\RequestResponse::initOnce(); + parent::__construct($data); + } + + /** + * Generated from protobuf field .google.protobuf.Timestamp time = 1 [(.gogoproto.stdtime) = true]; + * @return \Google\Protobuf\Timestamp|null + */ + public function getTime() + { + return $this->time; + } + + public function hasTime() + { + return isset($this->time); + } + + public function clearTime() + { + unset($this->time); + } + + /** + * Generated from protobuf field .google.protobuf.Timestamp time = 1 [(.gogoproto.stdtime) = true]; + * @param \Google\Protobuf\Timestamp $var + * @return $this + */ + public function setTime($var) + { + GPBUtil::checkMessage($var, \Google\Protobuf\Timestamp::class); + $this->time = $var; + + return $this; + } + +} + diff --git a/testing/api/testservice/Temporal/Api/Testservice/V1/LockTimeSkippingRequest.php b/testing/api/testservice/Temporal/Api/Testservice/V1/LockTimeSkippingRequest.php new file mode 100644 index 00000000..91504a95 --- /dev/null +++ b/testing/api/testservice/Temporal/Api/Testservice/V1/LockTimeSkippingRequest.php @@ -0,0 +1,31 @@ +temporal.api.testservice.v1.LockTimeSkippingRequest + */ +class LockTimeSkippingRequest extends \Google\Protobuf\Internal\Message +{ + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Temporal\Api\Testservice\V1\RequestResponse::initOnce(); + parent::__construct($data); + } + +} + diff --git a/testing/api/testservice/Temporal/Api/Testservice/V1/LockTimeSkippingResponse.php b/testing/api/testservice/Temporal/Api/Testservice/V1/LockTimeSkippingResponse.php new file mode 100644 index 00000000..4d216bc8 --- /dev/null +++ b/testing/api/testservice/Temporal/Api/Testservice/V1/LockTimeSkippingResponse.php @@ -0,0 +1,31 @@ +temporal.api.testservice.v1.LockTimeSkippingResponse + */ +class LockTimeSkippingResponse extends \Google\Protobuf\Internal\Message +{ + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Temporal\Api\Testservice\V1\RequestResponse::initOnce(); + parent::__construct($data); + } + +} + diff --git a/testing/api/testservice/Temporal/Api/Testservice/V1/SleepRequest.php b/testing/api/testservice/Temporal/Api/Testservice/V1/SleepRequest.php new file mode 100644 index 00000000..ecb611ce --- /dev/null +++ b/testing/api/testservice/Temporal/Api/Testservice/V1/SleepRequest.php @@ -0,0 +1,68 @@ +temporal.api.testservice.v1.SleepRequest + */ +class SleepRequest extends \Google\Protobuf\Internal\Message +{ + /** + * Generated from protobuf field .google.protobuf.Duration duration = 1 [(.gogoproto.stdduration) = true]; + */ + protected $duration = null; + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * @type \Google\Protobuf\Duration $duration + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Temporal\Api\Testservice\V1\RequestResponse::initOnce(); + parent::__construct($data); + } + + /** + * Generated from protobuf field .google.protobuf.Duration duration = 1 [(.gogoproto.stdduration) = true]; + * @return \Google\Protobuf\Duration|null + */ + public function getDuration() + { + return $this->duration; + } + + public function hasDuration() + { + return isset($this->duration); + } + + public function clearDuration() + { + unset($this->duration); + } + + /** + * Generated from protobuf field .google.protobuf.Duration duration = 1 [(.gogoproto.stdduration) = true]; + * @param \Google\Protobuf\Duration $var + * @return $this + */ + public function setDuration($var) + { + GPBUtil::checkMessage($var, \Google\Protobuf\Duration::class); + $this->duration = $var; + + return $this; + } + +} + diff --git a/testing/api/testservice/Temporal/Api/Testservice/V1/SleepResponse.php b/testing/api/testservice/Temporal/Api/Testservice/V1/SleepResponse.php new file mode 100644 index 00000000..fdc472dd --- /dev/null +++ b/testing/api/testservice/Temporal/Api/Testservice/V1/SleepResponse.php @@ -0,0 +1,31 @@ +temporal.api.testservice.v1.SleepResponse + */ +class SleepResponse extends \Google\Protobuf\Internal\Message +{ + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Temporal\Api\Testservice\V1\RequestResponse::initOnce(); + parent::__construct($data); + } + +} + diff --git a/testing/api/testservice/Temporal/Api/Testservice/V1/SleepUntilRequest.php b/testing/api/testservice/Temporal/Api/Testservice/V1/SleepUntilRequest.php new file mode 100644 index 00000000..587f9360 --- /dev/null +++ b/testing/api/testservice/Temporal/Api/Testservice/V1/SleepUntilRequest.php @@ -0,0 +1,68 @@ +temporal.api.testservice.v1.SleepUntilRequest + */ +class SleepUntilRequest extends \Google\Protobuf\Internal\Message +{ + /** + * Generated from protobuf field .google.protobuf.Timestamp timestamp = 1 [(.gogoproto.stdtime) = true]; + */ + protected $timestamp = null; + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * @type \Google\Protobuf\Timestamp $timestamp + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Temporal\Api\Testservice\V1\RequestResponse::initOnce(); + parent::__construct($data); + } + + /** + * Generated from protobuf field .google.protobuf.Timestamp timestamp = 1 [(.gogoproto.stdtime) = true]; + * @return \Google\Protobuf\Timestamp|null + */ + public function getTimestamp() + { + return $this->timestamp; + } + + public function hasTimestamp() + { + return isset($this->timestamp); + } + + public function clearTimestamp() + { + unset($this->timestamp); + } + + /** + * Generated from protobuf field .google.protobuf.Timestamp timestamp = 1 [(.gogoproto.stdtime) = true]; + * @param \Google\Protobuf\Timestamp $var + * @return $this + */ + public function setTimestamp($var) + { + GPBUtil::checkMessage($var, \Google\Protobuf\Timestamp::class); + $this->timestamp = $var; + + return $this; + } + +} + diff --git a/testing/api/testservice/Temporal/Api/Testservice/V1/TestServiceClient.php b/testing/api/testservice/Temporal/Api/Testservice/V1/TestServiceClient.php new file mode 100644 index 00000000..d5ee69d2 --- /dev/null +++ b/testing/api/testservice/Temporal/Api/Testservice/V1/TestServiceClient.php @@ -0,0 +1,161 @@ +_simpleRequest('/temporal.api.testservice.v1.TestService/LockTimeSkipping', + $argument, + ['\Temporal\Api\Testservice\V1\LockTimeSkippingResponse', 'decode'], + $metadata, $options); + } + + /** + * UnlockTimeSkipping decrements Time Locking Counter by one. + * + * If the counter reaches 0, it unlocks time skipping and fast forwards time. + * LockTimeSkipping and UnlockTimeSkipping calls are counted. Calling UnlockTimeSkipping does not + * guarantee that time is going to be fast forwarded as another lock can be holding it. + * + * Time Locking Counter can't be negative, unbalanced calls to UnlockTimeSkipping will lead to rpc call failure + * @param \Temporal\Api\Testservice\V1\UnlockTimeSkippingRequest $argument input argument + * @param array $metadata metadata + * @param array $options call options + * @return \Grpc\UnaryCall + */ + public function UnlockTimeSkipping(\Temporal\Api\Testservice\V1\UnlockTimeSkippingRequest $argument, + $metadata = [], $options = []) { + return $this->_simpleRequest('/temporal.api.testservice.v1.TestService/UnlockTimeSkipping', + $argument, + ['\Temporal\Api\Testservice\V1\UnlockTimeSkippingResponse', 'decode'], + $metadata, $options); + } + + /** + * This call returns only when the Test Server Time advances by the specified duration. + * This is an EXPERIMENTAL API. + * @param \Temporal\Api\Testservice\V1\SleepRequest $argument input argument + * @param array $metadata metadata + * @param array $options call options + * @return \Grpc\UnaryCall + */ + public function Sleep(\Temporal\Api\Testservice\V1\SleepRequest $argument, + $metadata = [], $options = []) { + return $this->_simpleRequest('/temporal.api.testservice.v1.TestService/Sleep', + $argument, + ['\Temporal\Api\Testservice\V1\SleepResponse', 'decode'], + $metadata, $options); + } + + /** + * This call returns only when the Test Server Time advances to the specified timestamp. + * If the current Test Server Time is beyond the specified timestamp, returns immediately. + * This is an EXPERIMENTAL API. + * @param \Temporal\Api\Testservice\V1\SleepUntilRequest $argument input argument + * @param array $metadata metadata + * @param array $options call options + * @return \Grpc\UnaryCall + */ + public function SleepUntil(\Temporal\Api\Testservice\V1\SleepUntilRequest $argument, + $metadata = [], $options = []) { + return $this->_simpleRequest('/temporal.api.testservice.v1.TestService/SleepUntil', + $argument, + ['\Temporal\Api\Testservice\V1\SleepResponse', 'decode'], + $metadata, $options); + } + + /** + * UnlockTimeSkippingWhileSleep decreases time locking counter by one and increases it back + * once the Test Server Time advances by the duration specified in the request. + * + * This call returns only when the Test Server Time advances by the specified duration. + * + * If it is called when Time Locking Counter is + * - more than 1 and no other unlocks are coming in, rpc call will block for the specified duration, time will not be fast forwarded. + * - 1, it will lead to fast forwarding of the time by the duration specified in the request and quick return of this rpc call. + * - 0 will lead to rpc call failure same way as an unbalanced UnlockTimeSkipping. + * @param \Temporal\Api\Testservice\V1\SleepRequest $argument input argument + * @param array $metadata metadata + * @param array $options call options + * @return \Grpc\UnaryCall + */ + public function UnlockTimeSkippingWithSleep(\Temporal\Api\Testservice\V1\SleepRequest $argument, + $metadata = [], $options = []) { + return $this->_simpleRequest('/temporal.api.testservice.v1.TestService/UnlockTimeSkippingWithSleep', + $argument, + ['\Temporal\Api\Testservice\V1\SleepResponse', 'decode'], + $metadata, $options); + } + + /** + * GetCurrentTime returns the current Temporal Test Server time + * + * This time might not be equal to {@link System#currentTimeMillis()} due to time skipping. + * @param \Google\Protobuf\GPBEmpty $argument input argument + * @param array $metadata metadata + * @param array $options call options + * @return \Grpc\UnaryCall + */ + public function GetCurrentTime(\Google\Protobuf\GPBEmpty $argument, + $metadata = [], $options = []) { + return $this->_simpleRequest('/temporal.api.testservice.v1.TestService/GetCurrentTime', + $argument, + ['\Temporal\Api\Testservice\V1\GetCurrentTimeResponse', 'decode'], + $metadata, $options); + } + +} diff --git a/testing/api/testservice/Temporal/Api/Testservice/V1/UnlockTimeSkippingRequest.php b/testing/api/testservice/Temporal/Api/Testservice/V1/UnlockTimeSkippingRequest.php new file mode 100644 index 00000000..cd9fb701 --- /dev/null +++ b/testing/api/testservice/Temporal/Api/Testservice/V1/UnlockTimeSkippingRequest.php @@ -0,0 +1,31 @@ +temporal.api.testservice.v1.UnlockTimeSkippingRequest + */ +class UnlockTimeSkippingRequest extends \Google\Protobuf\Internal\Message +{ + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Temporal\Api\Testservice\V1\RequestResponse::initOnce(); + parent::__construct($data); + } + +} + diff --git a/testing/api/testservice/Temporal/Api/Testservice/V1/UnlockTimeSkippingResponse.php b/testing/api/testservice/Temporal/Api/Testservice/V1/UnlockTimeSkippingResponse.php new file mode 100644 index 00000000..dca7a7ae --- /dev/null +++ b/testing/api/testservice/Temporal/Api/Testservice/V1/UnlockTimeSkippingResponse.php @@ -0,0 +1,31 @@ +temporal.api.testservice.v1.UnlockTimeSkippingResponse + */ +class UnlockTimeSkippingResponse extends \Google\Protobuf\Internal\Message +{ + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Temporal\Api\Testservice\V1\RequestResponse::initOnce(); + parent::__construct($data); + } + +} + diff --git a/testing/proto/dependencies/gogoproto/gogo.proto b/testing/proto/dependencies/gogoproto/gogo.proto new file mode 100644 index 00000000..49837cc6 --- /dev/null +++ b/testing/proto/dependencies/gogoproto/gogo.proto @@ -0,0 +1,141 @@ +// Protocol Buffers for Go with Gadgets +// +// Copyright (c) 2013, The GoGo Authors. All rights reserved. +// http://github.com/temporalio/gogo-protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto2"; +package gogoproto; + +import "google/protobuf/descriptor.proto"; + +option go_package = "github.com/gogo/protobuf/gogoproto"; + +extend google.protobuf.EnumOptions { + optional bool goproto_enum_prefix = 62001; + optional bool goproto_enum_stringer = 62021; + optional bool enum_stringer = 62022; + optional string enum_customname = 62023; + optional bool enumdecl = 62024; +} + +extend google.protobuf.EnumValueOptions { + optional string enumvalue_customname = 66001; +} + +extend google.protobuf.FileOptions { + optional bool goproto_getters_all = 63001; + optional bool goproto_enum_prefix_all = 63002; + optional bool goproto_stringer_all = 63003; + optional bool verbose_equal_all = 63004; + optional bool face_all = 63005; + optional bool gostring_all = 63006; + optional bool populate_all = 63007; + optional bool stringer_all = 63008; + optional bool onlyone_all = 63009; + + optional bool equal_all = 63013; + optional bool description_all = 63014; + optional bool testgen_all = 63015; + optional bool benchgen_all = 63016; + optional bool marshaler_all = 63017; + optional bool unmarshaler_all = 63018; + optional bool stable_marshaler_all = 63019; + + optional bool sizer_all = 63020; + + optional bool goproto_enum_stringer_all = 63021; + optional bool enum_stringer_all = 63022; + + optional bool unsafe_marshaler_all = 63023; + optional bool unsafe_unmarshaler_all = 63024; + + optional bool goproto_extensions_map_all = 63025; + optional bool goproto_unrecognized_all = 63026; + optional bool gogoproto_import = 63027; + optional bool protosizer_all = 63028; + optional bool compare_all = 63029; + optional bool typedecl_all = 63030; + optional bool enumdecl_all = 63031; + + optional bool goproto_registration = 63032; + optional bool messagename_all = 63033; + + optional bool goproto_sizecache_all = 63034; + optional bool goproto_unkeyed_all = 63035; +} + +extend google.protobuf.MessageOptions { + optional bool goproto_getters = 64001; + optional bool goproto_stringer = 64003; + optional bool verbose_equal = 64004; + optional bool face = 64005; + optional bool gostring = 64006; + optional bool populate = 64007; + optional bool stringer = 67008; + optional bool onlyone = 64009; + + optional bool equal = 64013; + optional bool description = 64014; + optional bool testgen = 64015; + optional bool benchgen = 64016; + optional bool marshaler = 64017; + optional bool unmarshaler = 64018; + optional bool stable_marshaler = 64019; + + optional bool sizer = 64020; + + optional bool unsafe_marshaler = 64023; + optional bool unsafe_unmarshaler = 64024; + + optional bool goproto_extensions_map = 64025; + optional bool goproto_unrecognized = 64026; + + optional bool protosizer = 64028; + optional bool compare = 64029; + + optional bool typedecl = 64030; + + optional bool messagename = 64033; + + optional bool goproto_sizecache = 64034; + optional bool goproto_unkeyed = 64035; +} + +extend google.protobuf.FieldOptions { + optional bool nullable = 65001; + optional bool embed = 65002; + optional string customtype = 65003; + optional string customname = 65004; + optional string jsontag = 65005; + optional string moretags = 65006; + optional string casttype = 65007; + optional string castkey = 65008; + optional string castvalue = 65009; + + optional bool stdtime = 65010; + optional bool stdduration = 65011; + optional bool wktpointer = 65012; +} diff --git a/testing/proto/temporal/api/testservice/v1/request_response.proto b/testing/proto/temporal/api/testservice/v1/request_response.proto new file mode 100644 index 00000000..572b337b --- /dev/null +++ b/testing/proto/temporal/api/testservice/v1/request_response.proto @@ -0,0 +1,63 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +syntax = "proto3"; + +package temporal.api.testservice.v1; + +option go_package = "go.temporal.io/api/testservice/v1;testservice"; +option java_package = "io.temporal.api.testservice.v1"; +option java_multiple_files = true; +option java_outer_classname = "RequestResponseProto"; +option ruby_package = "Temporal::Api::TestService::V1"; +option csharp_namespace = "Temporal.Api.TestService.V1"; + +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "dependencies/gogoproto/gogo.proto"; + +message LockTimeSkippingRequest { +} + +message LockTimeSkippingResponse { +} + +message UnlockTimeSkippingRequest { +} + +message UnlockTimeSkippingResponse { +} + +message SleepUntilRequest { + google.protobuf.Timestamp timestamp = 1 [(gogoproto.stdtime) = true]; +} + +message SleepRequest { + google.protobuf.Duration duration = 1 [(gogoproto.stdduration) = true]; +} + +message SleepResponse { +} + +message GetCurrentTimeResponse { + google.protobuf.Timestamp time = 1 [(gogoproto.stdtime) = true]; +} \ No newline at end of file diff --git a/testing/proto/temporal/api/testservice/v1/service.proto b/testing/proto/temporal/api/testservice/v1/service.proto new file mode 100644 index 00000000..5085ad87 --- /dev/null +++ b/testing/proto/temporal/api/testservice/v1/service.proto @@ -0,0 +1,90 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +syntax = "proto3"; + +package temporal.api.testservice.v1; + +option go_package = "go.temporal.io/api/testservice/v1;testservice"; +option java_package = "io.temporal.api.testservice.v1"; +option java_multiple_files = true; +option java_outer_classname = "ServiceProto"; +option ruby_package = "Temporal::Api::TestService::V1"; +option csharp_namespace = "Temporal.Api.TestService.V1"; + +import "temporal/api/testservice/v1/request_response.proto"; +import "google/protobuf/empty.proto"; + +// TestService API defines an interface supported only by the Temporal Test Server. +// It provides functionality needed or supported for testing purposes only. +// +// This is an EXPERIMENTAL API. +service TestService { + // LockTimeSkipping increments Time Locking Counter by one. + // + // If Time Locking Counter is positive, time skipping is locked (disabled). + // When time skipping is disabled, the time in test server is moving normally, with a real time pace. + // Test Server is typically started with locked time skipping and Time Locking Counter = 1. + // + // LockTimeSkipping and UnlockTimeSkipping calls are counted. + rpc LockTimeSkipping (LockTimeSkippingRequest) returns (LockTimeSkippingResponse) { + } + + // UnlockTimeSkipping decrements Time Locking Counter by one. + // + // If the counter reaches 0, it unlocks time skipping and fast forwards time. + // LockTimeSkipping and UnlockTimeSkipping calls are counted. Calling UnlockTimeSkipping does not + // guarantee that time is going to be fast forwarded as another lock can be holding it. + // + // Time Locking Counter can't be negative, unbalanced calls to UnlockTimeSkipping will lead to rpc call failure + rpc UnlockTimeSkipping (UnlockTimeSkippingRequest) returns (UnlockTimeSkippingResponse) { + } + + // This call returns only when the Test Server Time advances by the specified duration. + // This is an EXPERIMENTAL API. + rpc Sleep (SleepRequest) returns (SleepResponse) { + } + + // This call returns only when the Test Server Time advances to the specified timestamp. + // If the current Test Server Time is beyond the specified timestamp, returns immediately. + // This is an EXPERIMENTAL API. + rpc SleepUntil (SleepUntilRequest) returns (SleepResponse) { + } + + // UnlockTimeSkippingWhileSleep decreases time locking counter by one and increases it back + // once the Test Server Time advances by the duration specified in the request. + // + // This call returns only when the Test Server Time advances by the specified duration. + // + // If it is called when Time Locking Counter is + // - more than 1 and no other unlocks are coming in, rpc call will block for the specified duration, time will not be fast forwarded. + // - 1, it will lead to fast forwarding of the time by the duration specified in the request and quick return of this rpc call. + // - 0 will lead to rpc call failure same way as an unbalanced UnlockTimeSkipping. + rpc UnlockTimeSkippingWithSleep (SleepRequest) returns (SleepResponse) { + } + + // GetCurrentTime returns the current Temporal Test Server time + // + // This time might not be equal to {@link System#currentTimeMillis()} due to time skipping. + rpc GetCurrentTime (google.protobuf.Empty) returns (GetCurrentTimeResponse) { + } +} diff --git a/testing/scripts/generate-proto.php b/testing/scripts/generate-proto.php new file mode 100644 index 00000000..f88241ee --- /dev/null +++ b/testing/scripts/generate-proto.php @@ -0,0 +1,125 @@ +getMessage() . "\n"; + return; +} + +try { + echo 'grpc_php_plugin: '; + $plugin = Process::run('which', 'grpc_php_plugin'); + if (trim($plugin) === '') { + echo "not found\n"; + return; + } + + echo "{$plugin} [OK]\n"; +} catch (ProcessFailedException $e) { + echo $e->getMessage() . "\n"; + return; +} + + +echo 'api dir: '; +if (is_dir('api')) { + echo "exists\n"; +} else { + mkdir('api'); + echo "created\n"; +} + +echo "\nCompiling protobuf client...\n"; + +chdir(__DIR__ . '/../'); + +try { + echo "proto files lookup: "; + $files = Process::run( + 'find', + 'proto/temporal', + '-iname', + '*.proto' + ); + + $files = explode("\n", $files); + + echo "[OK]\n"; +} catch (ProcessFailedException $e) { + echo $e->getMessage() . "\n"; + return; +} + +try { + echo "generating client files: "; + $result = exec( + sprintf( + 'protoc --php_out=api/testservice --plugin=protoc-gen-grpc=%s --grpc_out=./api/testservice -Iproto %s', + $plugin, + join(' ', $files) + ) + ); + + if (trim($result) !== '') { + throw new Error($result); + } + + echo "[OK]\n"; +} catch (Error $e) { + echo $e->getMessage() . "\n"; + return; +} + +$gogo = file_get_contents('proto/dependencies/gogoproto/gogo.proto'); + +try { + echo "generating dependencies: "; + + // PHP does not support Syntax2 + file_put_contents( + 'proto/dependencies/gogoproto/gogo.proto', + str_replace('syntax = "proto2";', 'syntax = "proto3";', $gogo) + ); + + $result = exec( + sprintf( + 'protoc --php_out=api/testservice --plugin=protoc-gen-grpc=%s --grpc_out=./api/testservice -Iproto %s', + $plugin, + 'proto/dependencies/gogoproto/gogo.proto' + ) + ); + + if (trim($result) !== '') { + throw new Error($result); + } + + echo "[OK]\n"; +} catch (Error $e) { + echo $e->getMessage() . "\n"; + return; +} finally { + // restoring original file + file_put_contents('proto/dependencies/gogoproto/gogo.proto', $gogo); +} diff --git a/testing/src/Environment.php b/testing/src/Environment.php index af30849b..3d573679 100644 --- a/testing/src/Environment.php +++ b/testing/src/Environment.php @@ -18,7 +18,8 @@ final class Environment private ?Process $temporalServerProcess = null; private ?Process $roadRunnerProcess = null; - public function __construct(Output $output, Downloader $downloader, SystemInfo $systemInfo) { + public function __construct(Output $output, Downloader $downloader, SystemInfo $systemInfo) + { $this->downloader = $downloader; $this->systemInfo = $systemInfo; $this->output = $output; @@ -42,7 +43,9 @@ public function start(string $rrCommand = null): void } $this->output->write('Starting Temporal test server... '); - $this->temporalServerProcess = new Process([$this->systemInfo->temporalServerExecutable, 7233,]); + $this->temporalServerProcess = new Process( + [$this->systemInfo->temporalServerExecutable, 7233, '--enable-time-skipping'] + ); $this->temporalServerProcess->setTimeout(10); $this->temporalServerProcess->start(); $this->output->writeln('done.'); diff --git a/testing/src/TestService.php b/testing/src/TestService.php new file mode 100644 index 00000000..7a88c464 --- /dev/null +++ b/testing/src/TestService.php @@ -0,0 +1,125 @@ +testServiceClient = $testServiceClient; + } + + public static function create(string $host): self + { + return new self( + new TestServiceClient($host, ['credentials' => ChannelCredentials::createInsecure()]) + ); + } + + /** + * Increments Time Locking Counter by one. + * + * If Time Locking Counter is positive, time skipping is locked (disabled). + * When time skipping is disabled, the time in test server is moving normally, with a real time pace. + * Test Server is typically started with locked time skipping and Time Locking Counter = 1. + * + * lockTimeSkipping and unlockTimeSkipping calls are counted. + */ + public function lockTimeSkipping(): void + { + $this->invoke('LockTimeSkipping', new LockTimeSkippingRequest()); + } + + /** + * Decrements Time Locking Counter by one. + * + * If the counter reaches 0, it unlocks time skipping and fast forwards time. + * LockTimeSkipping and UnlockTimeSkipping calls are counted. Calling UnlockTimeSkipping does not + * guarantee that time is going to be fast forwarded as another lock can be holding it. + * + * Time Locking Counter can't be negative, unbalanced calls to unlockTimeSkipping will lead to a failure. + */ + public function unlockTimeSkipping(): void + { + $this->invoke('UnlockTimeSkipping', new UnlockTimeSkippingRequest()); + } + + /** + * Decreases time locking counter by one and increases it back. + * Once the Test Server Time advances by the duration specified in the request. + * + * This call returns only when the Test Server Time advances by the specified duration. + * + * If it is called when Time Locking Counter is + * - more than 1 and no other unlocks are coming in, rpc call will block for the specified duration, time will not be fast forwarded. + * - 1, it will lead to fast forwarding of the time by the duration specified in the request and quick return of this rpc call. + * - 0 will lead to rpc call failure same way as an unbalanced unlockTimeSkipping. + */ + public function unlockTimeSkippingWithSleep(int $seconds): void + { + $duration = (new Duration())->setSeconds($seconds); + $request = (new SleepRequest())->setDuration($duration); + $this->invoke('UnlockTimeSkippingWithSleep', $request); + } + + /** + * This call returns only when the Test Server Time advances by the specified duration. + * This is an EXPERIMENTAL API. + */ + public function sleep(int $seconds): void + { + $duration = (new Duration())->setSeconds($seconds); + $request = (new SleepRequest())->setDuration($duration); + $this->invoke('Sleep', $request); + } + + /** + * This call returns only when the Test Server Time advances to the specified timestamp. + * If the current Test Server Time is beyond the specified timestamp, returns immediately. + * This is an EXPERIMENTAL API. + */ + public function sleepUntil(int $timestamp): void + { + $request = (new SleepUntilRequest())->setTimestamp((new Timestamp())->setSeconds($timestamp)); + $this->invoke('sleepUntil', $request); + } + + /** + * GetCurrentTime returns the current Temporal Test Server time + */ + public function getCurrentTime(): Carbon + { + /** @var GetCurrentTimeResponse $result */ + $result = $this->invoke('GetCurrentTime', new GPBEmpty()); + return Carbon::createFromTimestamp($result->getTime()->getSeconds()); + } + + private function invoke(string $method, object $request): object + { + $call = $this->testServiceClient->{$method}($request); + [$result, $status] = $call->wait(); + + if ($status->code !== 0) { + throw new ServiceClientException($status); + } + + return $result; + } +} diff --git a/testing/src/WithoutTimeSkipping.php b/testing/src/WithoutTimeSkipping.php new file mode 100644 index 00000000..87689bd4 --- /dev/null +++ b/testing/src/WithoutTimeSkipping.php @@ -0,0 +1,23 @@ +testService = TestService::create('localhost:7233'); + $this->testService->lockTimeSkipping(); + parent::setUp(); + } + + protected function tearDown(): void + { + $this->testService->unlockTimeSkipping(); + parent::tearDown(); + } +} diff --git a/testing/src/WorkflowTestCase.php b/testing/src/WorkflowTestCase.php index ea8ad51e..1f73fe12 100644 --- a/testing/src/WorkflowTestCase.php +++ b/testing/src/WorkflowTestCase.php @@ -11,12 +11,13 @@ class WorkflowTestCase extends TestCase { protected WorkflowClient $workflowClient; + protected TestService $testingService; protected function setUp(): void { - $this->workflowClient = new WorkflowClient( - ServiceClient::create('localhost:7233') - ); + $this->workflowClient = new WorkflowClient(ServiceClient::create('localhost:7233')); + $this->testingService = TestService::create('localhost:7233'); + parent::setUp(); } } diff --git a/tests/Functional/Client/AwaitTestCase.php b/tests/Functional/Client/AwaitTestCase.php index da120bf4..2725e1d8 100644 --- a/tests/Functional/Client/AwaitTestCase.php +++ b/tests/Functional/Client/AwaitTestCase.php @@ -11,11 +11,13 @@ namespace Temporal\Tests\Functional\Client; +use Temporal\Api\Testservice\V1\TestServiceClient; use Temporal\DataConverter\Type; use Temporal\Exception\Client\WorkflowFailedException; use Temporal\Exception\Failure\ActivityFailure; use Temporal\Exception\Failure\ApplicationFailure; use Temporal\Exception\Failure\CanceledFailure; +use Temporal\Testing\TestService; use Temporal\Tests\Workflow\AggregatedWorkflow; use Temporal\Tests\Workflow\LoopWithSignalCoroutinesWorkflow; use Temporal\Tests\Workflow\LoopWorkflow; diff --git a/tests/Functional/Client/ClientTestCase.php b/tests/Functional/Client/ClientTestCase.php index f1327f75..b0659473 100644 --- a/tests/Functional/Client/ClientTestCase.php +++ b/tests/Functional/Client/ClientTestCase.php @@ -16,6 +16,7 @@ use Temporal\Api\Workflowservice\V1\GetWorkflowExecutionHistoryRequest; use Temporal\Client\GRPC\ServiceClient; use Temporal\Client\WorkflowClient; +use Temporal\Testing\WithoutTimeSkipping; use Temporal\Tests\Functional\FunctionalTestCase; use Temporal\Workflow\WorkflowExecution; @@ -24,6 +25,8 @@ */ abstract class ClientTestCase extends FunctionalTestCase { + use WithoutTimeSkipping; + /** * @param string $connection * @return WorkflowClient