Skip to content

Commit 6cc23da

Browse files
author
Ace Nassri
authored
feat(functions): add helloworld pubsub sample (GoogleCloudPlatform#1262)
Related to b/154660288 (🌔 Pardon the COVID-schedule submit time!)
1 parent 773b391 commit 6cc23da

File tree

5 files changed

+350
-0
lines changed

5 files changed

+350
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"require": {
3+
"google/cloud-functions-framework": "^0.7.1"
4+
},
5+
"require-dev": {
6+
"google/cloud-pubsub": "^1.29",
7+
"google/cloud-logging": "^1.21"
8+
}
9+
}

functions/helloworld_pubsub/index.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
/**
3+
* Copyright 2021 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// [START functions_helloworld_pubsub]
19+
20+
use Google\CloudFunctions\CloudEvent;
21+
22+
function helloworldPubsub(CloudEvent $event): string
23+
{
24+
$log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');
25+
26+
$data = $event->getData();
27+
if (isset($data['data'])) {
28+
$name = htmlspecialchars(base64_decode($data['data']));
29+
} else {
30+
$name = 'World';
31+
}
32+
33+
$result = 'Hello, ' . $name . '!';
34+
fwrite($log, $result . PHP_EOL);
35+
return $result;
36+
}
37+
// [END functions_helloworld_pubsub]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
xml version="1.0" encoding="UTF-8"?>
2+
17+
<phpunit bootstrap="../../testing/bootstrap.php" convertWarningsToExceptions="false">
18+
<testsuites>
19+
<testsuite name="Cloud Functions Helloworld Pub/Sub Test Suite">
20+
<directory>testdirectory>
21+
testsuite>
22+
testsuites>
23+
<logging>
24+
<log type="coverage-clover" target="build/logs/clover.xml"/>
25+
logging>
26+
<filter>
27+
<whitelist>
28+
<directory suffix=".php">.directory>
29+
<exclude>
30+
<directory>./vendordirectory>
31+
exclude>
32+
whitelist>
33+
filter>
34+
phpunit>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
2+
/**
3+
* Copyright 2021 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\HelloworldPubsub\Test;
21+
22+
use Google\Cloud\Logging\LoggingClient;
23+
use Google\Cloud\TestUtils\CloudFunctionDeploymentTrait;
24+
use Google\Cloud\TestUtils\EventuallyConsistentTestTrait;
25+
use Google\Cloud\TestUtils\GcloudWrapper\CloudFunction;
26+
use Google\Cloud\PubSub\PubSubClient;
27+
use PHPUnit\Framework\TestCase;
28+
use PHPUnit\Framework\ExpectationFailedException;
29+
30+
/**
31+
* Class DeployTest.
32+
*
33+
* This test is not run by the CI system.
34+
*
35+
* To skip deployment of a new function, run with "GOOGLE_SKIP_DEPLOYMENT=true".
36+
* To skip deletion of the tested function, run with "GOOGLE_KEEP_DEPLOYMENT=true".
37+
*/
38+
class DeployTest extends TestCase
39+
{
40+
use CloudFunctionDeploymentTrait;
41+
use EventuallyConsistentTestTrait;
42+
43+
private static $entryPoint = 'helloworldPubsub';
44+
45+
/* var string */
46+
private static $projectId;
47+
48+
/* var string */
49+
private static $topicName;
50+
51+
/** @var LoggingClient */
52+
private static $loggingClient;
53+
54+
public function dataProvider()
55+
{
56+
return [
57+
[
58+
'name' => '',
59+
'expected' => 'Hello, World!',
60+
'label' => 'Should print a default value'
61+
],
62+
[
63+
'name' => 'John',
64+
'expected' => 'Hello, John!',
65+
'label' => 'Should print a name'
66+
],
67+
];
68+
}
69+
70+
/**
71+
* @dataProvider dataProvider
72+
*/
73+
public function testHelloworldPubsub(string $name, string $expected, string $label): void
74+
{
75+
// Send Pub/Sub message.
76+
$this->publishMessage($name);
77+
78+
// Give event and log systems a head start.
79+
// If log retrieval fails to find logs for our function within retry limit, increase sleep time.
80+
sleep(60);
81+
82+
$fiveMinAgo = date(\DateTime::RFC3339, strtotime('-5 minutes'));
83+
$this->processFunctionLogs(self::$fn, $fiveMinAgo, function (\Iterator $logs) use ($name, $expected, $label) {
84+
// Concatenate all relevant log messages.
85+
$actual = '';
86+
foreach ($logs as $log) {
87+
$info = $log->info();
88+
$actual .= $info['textPayload'];
89+
}
90+
91+
$expected = 'Hello, ' . $name . '!';
92+
$this->assertContains($expected, $actual, $label);
93+
});
94+
}
95+
96+
private function publishMessage(string $name): void
97+
{
98+
// Publish a message to trigger the function.
99+
$pubsub = new PubSubClient();
100+
$topic = $pubsub->topic(self::$topicName);
101+
$topic->publish([
102+
'data' => $name,
103+
'attributes' => [
104+
'foo' => 'bar'
105+
]
106+
]);
107+
}
108+
109+
/**
110+
* Retrieve and process logs for the defined function.
111+
*
112+
* @param CloudFunction $fn function whose logs should be checked.
113+
* @param string $startTime RFC3339 timestamp marking start of time range to retrieve.
114+
* @param callable $process callback function to run on the logs.
115+
*/
116+
private function processFunctionLogs(CloudFunction $fn, string $startTime, callable $process)
117+
{
118+
$projectId = self::requireEnv('GOOGLE_PROJECT_ID');
119+
120+
if (empty(self::$loggingClient)) {
121+
self::$loggingClient = new LoggingClient([
122+
'projectId' => $projectId
123+
]);
124+
}
125+
126+
// Define the log search criteria.
127+
$logFullName = 'projects/' . $projectId . '/logs/cloudfunctions.googleapis.com%2Fcloud-functions';
128+
$filter = sprintf(
129+
'logName="%s" resource.labels.function_name="%s" timestamp>="%s"',
130+
$logFullName,
131+
$fn->getFunctionName(),
132+
$startTime
133+
);
134+
135+
echo "\nRetrieving logs [$filter]...\n";
136+
137+
// Check for new logs for the function.
138+
$attempt = 1;
139+
$this->runEventuallyConsistentTest(function () use ($filter, $process, &$attempt) {
140+
$entries = self::$loggingClient->entries(['filter' => $filter]);
141+
142+
// If no logs came in try again.
143+
if (empty($entries->current())) {
144+
echo 'Logs not found, attempting retry #' . $attempt++ . PHP_EOL;
145+
throw new ExpectationFailedException('Log Entries not available');
146+
}
147+
echo 'Processing logs...' . PHP_EOL;
148+
149+
$process($entries);
150+
}, $retries = 10);
151+
}
152+
153+
/**
154+
* Deploy the Cloud Function, called from DeploymentTrait::deployApp().
155+
*
156+
* Overrides CloudFunctionDeploymentTrait::doDeploy().
157+
*/
158+
private static function doDeploy()
159+
{
160+
self::$projectId = self::requireEnv('GOOGLE_CLOUD_PROJECT');
161+
self::$topicName = self::requireEnv('FUNCTIONS_TOPIC');
162+
163+
return self::$fn->deploy([], '--trigger-topic=' . self::$topicName);
164+
}
165+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
2+
/**
3+
* Copyright 2021 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\HelloworldPubsub\Test;
21+
22+
use PHPUnit\Framework\TestCase;
23+
use Google\Cloud\TestUtils\CloudFunctionLocalTestTrait;
24+
25+
/**
26+
* Class IntegrationTest.
27+
*
28+
* Integration Test for helloworldPubsub.
29+
*/
30+
class IntegrationTest extends TestCase
31+
{
32+
use CloudFunctionLocalTestTrait;
33+
34+
/** @var string */
35+
private static $entryPoint = 'helloworldPubsub';
36+
37+
/** @var string */
38+
private static $functionSignatureType = 'cloudevent';
39+
40+
public function dataProvider()
41+
{
42+
return [
43+
[
44+
'cloudevent' => [
45+
'id' => uniqid(),
46+
'source' => 'pubsub.googleapis.com',
47+
'specversion' => '1.0',
48+
'type' => 'google.cloud.pubsub.topic.v1.messagePublished',
49+
],
50+
'data' => [],
51+
'statusCode' => 200,
52+
'expected' => 'Hello, World!',
53+
'label' => 'Should print a default value'
54+
],
55+
[
56+
'cloudevent' => [
57+
'id' => uniqid(),
58+
'source' => 'pubsub.googleapis.com',
59+
'specversion' => '1.0',
60+
'type' => 'google.cloud.pubsub.topic.v1.messagePublished',
61+
],
62+
'data' => [
63+
'data' => base64_encode('John')
64+
],
65+
'statusCode' => 200,
66+
'expected' => 'Hello, John!',
67+
'label' => 'Should print a name'
68+
],
69+
];
70+
}
71+
72+
/**
73+
* @dataProvider dataProvider
74+
*/
75+
public function testHelloworldPubsub(array $cloudevent, array $data, string $statusCode, string $expected, string $label): void
76+
{
77+
// Prepare the HTTP headers for a CloudEvent.
78+
$cloudEventHeaders = [];
79+
foreach ($cloudevent as $key => $value) {
80+
$cloudEventHeaders['ce-' . $key] = $value;
81+
}
82+
83+
// Send an HTTP request using CloudEvent metadata.
84+
$resp = $this->client->request('POST', '/', [
85+
'body' => json_encode($data),
86+
'headers' => $cloudEventHeaders + [
87+
// Instruct the function framework to parse the body as JSON.
88+
'content-type' => 'application/json'
89+
],
90+
]);
91+
92+
// The Cloud Function logs all data to stderr.
93+
$actual = self::$localhost->getIncrementalErrorOutput();
94+
95+
// Confirm the status code.
96+
$this->assertEquals(
97+
$statusCode,
98+
$resp->getStatusCode(),
99+
$label . ' status code'
100+
);
101+
102+
// Verify the function's behavior is correct.
103+
$this->assertContains($expected, $actual, $label . ' contains');
104+
}
105+
}

0 commit comments

Comments
 (0)