Skip to content

feat: implement storage_system_test example #276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ gcloud projects add-iam-policy-binding "${GOOGLE_CLOUD_PROJECT}" \

## Verify the Resources

Create the buildpack builder:
Create the buildpack builder. Note that this uses the *CI* image, because
you (most likely) want to build with the current version of the framework,
not with the last release:

```sh
cd functions-framework-cpp
Expand Down
5 changes: 1 addition & 4 deletions examples/site/hello_world_storage/hello_world_storage.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,14 @@

// [START functions_helloworld_storage]
#include <google/cloud/functions/cloud_event.h>
#include <boost/archive/iterators/binary_from_base64.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
#include <iostream>

namespace gcf = ::google::cloud::functions;

void hello_world_storage(gcf::CloudEvent event) { // NOLINT
if (event.data_content_type().value_or("") != "application/json") {
std::cerr << "Error: expected application/json data\n";
BOOST_LOG_TRIVIAL(error) << "expected application/json data";
return;
}
auto const payload = nlohmann::json::parse(event.data().value_or("{}"));
Expand Down
1 change: 0 additions & 1 deletion examples/site/hello_world_storage/vcpkg.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"version-string": "unversioned",
"dependencies": [
"boost-log",
"boost-serialization",
"functions-framework-cpp",
"nlohmann-json"
]
Expand Down
27 changes: 13 additions & 14 deletions examples/site/testing_storage/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@

if (BUILD_TESTING)
find_package(GTest CONFIG REQUIRED)
find_package(fmt CONFIG REQUIRED)
find_package(google_cloud_cpp_storage REQUIRED)

set(functions_framework_cpp_examples_unit_tests
# cmake-format: sort
storage_integration_test.cc storage_unit_test.cc)

set(functions_framework_cpp_examples_programs # cmake-format: sort
storage_integration_server.cc)

foreach (fname ${functions_framework_cpp_examples_unit_tests})
string(REPLACE "/" "_" target "${fname}")
string(REPLACE ".cc" "" target "${target}")
Expand All @@ -40,15 +40,14 @@ if (BUILD_TESTING)
add_test(NAME ${target} COMMAND ${target})
endforeach ()

foreach (fname ${functions_framework_cpp_examples_programs})
string(REPLACE "/" "_" target "${fname}")
string(REPLACE ".cc" "" target "${target}")
add_executable("${target}" ${fname})
functions_framework_cpp_add_common_options(${target})
target_link_libraries(
${target}
PRIVATE functions_framework_examples
functions-framework-cpp::framework Boost::filesystem
Boost::log)
endforeach ()
add_executable(storage_integration_server storage_integration_server.cc)
target_link_libraries(
storage_integration_server
PRIVATE functions_framework_examples functions-framework-cpp::framework
Boost::filesystem Boost::log)
add_executable(storage_system_test storage_system_test.cc)
target_link_libraries(
storage_system_test
PRIVATE google-cloud-cpp::storage fmt::fmt Boost::log GTest::gmock_main
GTest::gmock GTest::gtest)
endif ()
146 changes: 146 additions & 0 deletions examples/site/testing_storage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# How-to Guide: Testing Event-driven Functions (Cloud Audit Log triggered)

> :warning: this example depends on features in the upcoming release 0.4.0.
> until that release is cut, the default builders will link against 0.3.0
> and this example will not work correctly.

[buildpacks]: https://buildpacks.io
[boost-log-gh]: https://github.com/boostorg/log
[/examples/site/hello_world_storage/hello_world_storage.cc]: /examples/site/hello_world_storage/hello_world_storage.cc
[storage_unit_test.cc]: storage_unit_test.cc
[storage_integration_server.cc]: storage_integration_server.cc
[storage_integration_test.cc]: storage_integration_test.cc
[quickstart-guide]: /examples/site/howto_local_development/README.md
[container-guide]: /examples/site/howto_create_container/README.md
[pubsub-quickstart]: https://cloud.google.com/pubsub/docs/quickstart-console

Event-driven functions do not return values, therefore, their only observable
behavior are their side-effects and any testing for them is based in observing
these side-effects. In this example we will examine the log of a event-driven
function to test it. Depending on where the function is deployed this might be
more or less involved.

## Function under Test

We will use this function throughout this guide:

[/examples/site/hello_world_storage/hello_world_storage.cc]
```cc
#include <google/cloud/functions/cloud_event.h>
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>

namespace gcf = ::google::cloud::functions;

void hello_world_storage(gcf::CloudEvent event) { // NOLINT
if (event.data_content_type().value_or("") != "application/json") {
BOOST_LOG_TRIVIAL(error) << "expected application/json data";
return;
}
auto const payload = nlohmann::json::parse(event.data().value_or("{}"));
BOOST_LOG_TRIVIAL(info) << "Event: " << event.id();
BOOST_LOG_TRIVIAL(info) << "Event Type: " << event.type();
BOOST_LOG_TRIVIAL(info) << "Bucket: " << payload.value("bucket", "");
BOOST_LOG_TRIVIAL(info) << "Object: " << payload.value("name", "");
BOOST_LOG_TRIVIAL(info) << "Metageneration: "
<< payload.value("metageneration", "");
BOOST_LOG_TRIVIAL(info) << "Created: " << payload.value("timeCreated", "");
BOOST_LOG_TRIVIAL(info) << "Updated: " << payload.value("updated", "");
}
```

This test receives storage events and logs a few of the fields in these
events.

## Writing a Unit Test

This function uses the [Boost.Log][boost-log-gh] library to facilitate unit
testing. Logging can be captured and examined using the usual testing
assertions. See [storage_unit_test.cc] for more details.

## Writing an Integration Test

To write an integration test we first create a local server to run the
function, as shown in [storage_integration_server.cc], then we launch this
server from within the [integration test][storage_integration_test.cc] and
examine its result.

## Writing a System Test

### Pre-requisites

This guide assumes you are familiar with deploying C++ functions to Cloud Run.
If necessary consult the [Howto Guide][container-guide] for more information.
We will create a container for the storage "hello world" function as usual:

```shell
pack build \
--builder gcf-cpp-builder:bionic \
--env "FUNCTION_SIGNATURE_TYPE=cloudevent" \
--env "TARGET_FUNCTION=hello_world_storage" \
--path "examples/site/hello_world_storage" \
"gcr.io/${GOOGLE_CLOUD_PROJECT}/gcf-hello-world-storage"
```

Then deploy this function to Cloud Run:

```shell
docker push "gcr.io/${GOOGLE_CLOUD_PROJECT}/gcf-hello-world-storage"
gcloud run deploy gcf-hello-world-storage \
--project="${GOOGLE_CLOUD_PROJECT}" \
--image="gcr.io/${GOOGLE_CLOUD_PROJECT}/gcf-hello-world-storage:latest" \
--region="us-central1" \
--platform="managed" \
--allow-unauthenticated
```

If needed, create a Google Cloud Storage bucket for these tests:

```shell
BUCKET_NAME=... # assign a valid bucket name
gsutil mb -p "${GOOGLE_CLOUD_PROJECT}" "gs://${BUCKET_NAME}"
```

Then change the bucket to publish all changes to a Cloud Pub/Sub topic. This
command will create the topic if needed, and assign the right permissions:

```shell
TOPIC_ID=... # set this variable to a valid topic id.
gsutil notification create -f json -t "${TOPIC_ID}" "gs://${BUCKET_NAME}"
```

Setup a Cloud Pub/Sub trigger for your function when messages appear on this
topic:

```shell
gcloud beta eventarc triggers create gcf-hello-world-storage-trigger \
--project="${GOOGLE_CLOUD_PROJECT}" \
--location="us-central1" \
--destination-run-service="gcf-hello-world-storage" \
--destination-run-region="us-central1" \
--matching-criteria="type=google.cloud.pubsub.topic.v1.messagePublished" \
--transport-topic="${TOPIC_ID}"
```

Finally, you can run the system test, using environment variables to pass
any configuration parameters:

```shell
env "GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT}" \
"BUCKET_NAME=${BUCKET_NAME}" \
"SERVICE_ID=gcf-hello-world-storage" \
./pubsub_system_test # use actual path to binary
```

## Cleanup

Remember to remove your deployment and the image once you have finished:

```sh
gcloud run services delete gcf-cpp-hello-world-storage \
--project="${GOOGLE_CLOUD_PROJECT}" \
--region="us-central1" \
--platform="managed"
gcloud container images delete \
"gcr.io/${GOOGLE_CLOUD_PROJECT}/gcf-cpp-hello-world-storage:latest"
```
106 changes: 106 additions & 0 deletions examples/site/testing_storage/storage_system_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// [START functions_storage_system_test]
#include <google/cloud/storage/client.h>
#include <boost/process.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <chrono>
#include <random>
#include <string>
#include <thread>

namespace {

namespace bp = ::boost::process;
namespace gcs = ::google::cloud::storage;
using ::testing::HasSubstr;

class StorageSystemTest : public ::testing::Test {
protected:
void SetUp() override {
// This test can only run if it is properly configured.
auto const* project_id = std::getenv("GOOGLE_CLOUD_PROJECT");
ASSERT_NE(project_id, nullptr);
project_id_ = project_id;

auto const* bucket_name = std::getenv("BUCKET_NAME");
ASSERT_NE(bucket_name, nullptr);
bucket_name_ = bucket_name;

auto const* service_id = std::getenv("SERVICE_ID");
ASSERT_NE(service_id, nullptr);
service_id_ = service_id;
}

void TearDown() override {}

[[nodiscard]] std::string const& project_id() const { return project_id_; }
[[nodiscard]] std::string const& bucket_name() const { return bucket_name_; }
[[nodiscard]] std::string const& service_id() const { return service_id_; }

private:
std::string project_id_;
std::string bucket_name_;
std::string service_id_;
};

TEST_F(StorageSystemTest, Basic) {
auto client = gcs::Client::CreateDefaultClient();
ASSERT_TRUE(client.status().ok());

// Use a random name to avoid interference from other tests.
auto gen = std::mt19937_64(std::random_device{}());
auto rnd = [&gen] {
return std::to_string(std::uniform_int_distribution<std::uint64_t>{}(gen));
};
auto const object_name = "test-" + rnd() + '-' + rnd() + '-' + rnd();
auto const expected = "Object: " + object_name;
SCOPED_TRACE("Testing for " + object_name);
auto meta =
client->InsertObject(bucket_name(), object_name, "Lorem ipsum...");
ASSERT_TRUE(meta.status().ok());

std::vector<std::string> lines;
// It may take a few seconds for the logs to propagate, so try a few times.
using namespace std::chrono_literals;
for (auto delay : {1s, 2s, 4s, 8s, 16s}) {
bp::ipstream out;
auto constexpr kProject = "--project={}";
auto constexpr kLogFilter =
"resource.type=cloud_run_revision AND "
"resource.labels.service_name={} AND "
"logName:stdout";
auto reader = bp::child(bp::search_path("gcloud"), "logging", "read",
fmt::format(kProject, project_id()),
fmt::format(kLogFilter, service_id()),
"--format=value(textPayload)", "--limit=100",
(bp::std_out & bp::std_err) > out);
reader.wait();
ASSERT_EQ(reader.exit_code(), 0);
for (std::string l; std::getline(out, l);) lines.push_back(std::move(l));
auto count = std::count_if(
lines.begin(), lines.end(),
[s = expected](auto l) { return l.find(s) != std::string::npos; });
if (count != 0) break;
std::this_thread::sleep_for(delay);
}
EXPECT_THAT(lines, Contains(HasSubstr(expected)));
EXPECT_TRUE(client->DeleteObject(bucket_name(), object_name).ok());
}

} // namespace

// [END functions_storage_system_test]