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