blob: 6ba0d93833204412738504589adb4accdfd5dbaa [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors
//
// 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.
#include "encoder/encoder.h"
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <utility>
#include "encoder/client_secret.h"
#include "encoder/project_context.h"
#include "third_party/googletest/googletest/include/gtest/gtest.h"
namespace cobalt {
namespace encoder {
using config::EncodingRegistry;
using config::MetricRegistry;
namespace {
const uint32_t kCustomerId = 1;
const uint32_t kProjectId = 1;
// This unix timestamp corresponds to Friday Dec 2, 2016 in UTC
// and Thursday Dec 1, 2016 in Pacific time.
const time_t kSomeTimestamp = 1480647356;
// This is the day index for Friday Dec 2, 2016
const uint32_t kUtcDayIndex = 17137;
// This is the day index for Thurs Dec 1, 2016
const uint32_t kPacificDayIndex = 17136;
const char* kMetricConfigText = R"(
# Metric 1 has one string part, and local time_zone_policy.
element {
customer_id: 1
project_id: 1
id: 1
time_zone_policy: LOCAL
parts {
key: "Part1"
value {
}
}
}
# Metric 2 has one integer part, and UTC time_zone_policy.
element {
customer_id: 1
project_id: 1
id: 2
time_zone_policy: UTC
parts {
key: "Part1"
value {
data_type: INT
}
}
}
# Metric 3 has one blob part, and local time_zone_policy.
element {
customer_id: 1
project_id: 1
id: 3
time_zone_policy: LOCAL
parts {
key: "Part1"
value {
data_type: BLOB
}
}
}
# Metric 4 has one String part and one int part, and UTC time_zone_policy.
element {
customer_id: 1
project_id: 1
id: 4
time_zone_policy: UTC
parts {
key: "city"
value {
}
}
parts {
key: "rating"
value {
data_type: INT
}
}
}
# Metric 5 is missing a time_zone_policy
element {
customer_id: 1
project_id: 1
id: 5
parts {
key: "Part1"
value {
}
}
}
# Metric 6 has in INDEX part.
element {
customer_id: 1
project_id: 1
id: 6
time_zone_policy: UTC
parts {
key: "Part1"
value {
data_type: INDEX
}
}
}
)";
const char* kEncodingConfigText = R"(
# EncodingConfig 1 is Forculus.
element {
customer_id: 1
project_id: 1
id: 1
forculus {
threshold: 20
}
}
# EncodingConfig 2 is String RAPPOR.
element {
customer_id: 1
project_id: 1
id: 2
rappor {
num_bloom_bits: 8
num_hashes: 2
num_cohorts: 20
prob_0_becomes_1: 0.25
prob_1_stays_1: 0.75
}
}
# EncodingConfig 3 is Basic RAPPOR with string categories.
element {
customer_id: 1
project_id: 1
id: 3
basic_rappor {
prob_0_becomes_1: 0.25
prob_1_stays_1: 0.75
string_categories: {
category: "Apple"
category: "Banana"
category: "Cantaloupe"
}
}
}
# EncodingConfig 4 is Basic RAPPOR with int categories.
element {
customer_id: 1
project_id: 1
id: 4
basic_rappor {
prob_0_becomes_1: 0.25
prob_1_stays_1: 0.75
int_range_categories: {
first: 123
last: 234
}
}
}
# EncodingConfig 5 is Forculus with a missing threshold.
element {
customer_id: 1
project_id: 1
id: 5
forculus {
}
}
# EncodingConfig 6 is String RAPPOR with many missing values.
element {
customer_id: 1
project_id: 1
id: 6
rappor {
}
}
# EncodingConfig 7 is the NoOp encoding.
element {
customer_id: 1
project_id: 1
id: 7
no_op_encoding {
}
}
# EncodingConfig 8 is Basic RAPPOR with 5 INDEXed categories.
element {
customer_id: 1
project_id: 1
id: 8
basic_rappor {
prob_0_becomes_1: 0.25
prob_1_stays_1: 0.75
indexed_categories: {
num_categories: 5,
}
}
}
)";
// Returns a ProjectContext obtained by parsing the above configuration
// text strings.
std::shared_ptr<ProjectContext> GetTestProject() {
// Parse the metric config string
auto metric_parse_result =
MetricRegistry::FromString(kMetricConfigText, nullptr);
EXPECT_EQ(config::kOK, metric_parse_result.second);
std::shared_ptr<MetricRegistry> metric_registry(
metric_parse_result.first.release());
// Parse the encoding config string
auto encoding_parse_result =
EncodingRegistry::FromString(kEncodingConfigText, nullptr);
EXPECT_EQ(config::kOK, encoding_parse_result.second);
std::shared_ptr<EncodingRegistry> encoding_registry(
encoding_parse_result.first.release());
return std::shared_ptr<ProjectContext>(new ProjectContext(
kCustomerId, kProjectId, metric_registry, encoding_registry));
}
} // namespace
// Checks |result|: Checks that the status is kOK, that the observation and
// metadata are not null, that the observation has a single part named
// "Part1", that it uses the expected encoding and that it is not empty.
// |expect_utc| should be true to indicate that it is expected that the
// day index was computed using UTC.
void CheckSinglePartResult(
const Encoder::Result& result, uint32_t expected_metric_id,
uint32_t expected_encoding_config_id, bool expect_utc,
const ObservationPart::ValueCase& expected_encoding) {
ASSERT_EQ(Encoder::kOK, result.status);
ASSERT_NE(nullptr, result.observation);
ASSERT_NE(nullptr, result.metadata);
EXPECT_EQ(kCustomerId, result.metadata->customer_id());
EXPECT_EQ(kProjectId, result.metadata->project_id());
EXPECT_EQ(expected_metric_id, result.metadata->metric_id());
if (expect_utc) {
EXPECT_EQ(kUtcDayIndex, result.metadata->day_index());
} else {
// Only perform the following check when running this test in the Pacific
// timezone. Note that |timezone| is a global variable defined in <ctime>
// that stores difference between UTC and the latest local standard time, in
// seconds west of UTC. This value is not adjusted for daylight saving.
// See https://www.gnu.org/software/libc/manual/html_node/ \
// Time-Zone-Functions.html#Time-Zone-Functions
if (timezone / 3600 == 8) {
EXPECT_EQ(kPacificDayIndex, result.metadata->day_index());
}
}
const Observation& observation = *result.observation;
// The Metric specified has only one part named "Part1" so the encoded
// observation should have one part named "Part1".
ASSERT_EQ(1, observation.parts_size());
const ObservationPart& obs_part = observation.parts().at("Part1");
// The observation part should use the right encoding.
EXPECT_EQ(expected_encoding_config_id, obs_part.encoding_config_id());
EXPECT_EQ(expected_encoding, obs_part.value_case());
// We sanity test the Observation by checking that it is not empty.
switch (expected_encoding) {
case ObservationPart::kForculus: {
EXPECT_NE("", obs_part.forculus().ciphertext());
break;
}
case ObservationPart::kRappor: {
EXPECT_NE("", obs_part.rappor().data());
break;
}
case ObservationPart::kBasicRappor: {
EXPECT_NE("", obs_part.basic_rappor().data());
break;
}
case ObservationPart::kUnencoded: {
EXPECT_TRUE(obs_part.unencoded().has_unencoded_value());
break;
}
default:
EXPECT_TRUE(false) << " Unexpected case";
}
}
// Tests the EncodeString() method using the given |value| and the given
// metric and encoding. The metric is expected to have a single part named
// "Part1". We validate that there are no errors and that the
// produced Observation has the |expected_type| and is non-empty.
// Returns the encoded Observation.
Observation DoEncodeStringTest(
std::string value, uint32_t metric_id, uint32_t encoding_config_id,
bool expect_utc, const ObservationPart::ValueCase& expected_encoding) {
// Build the ProjectContext encapsulating our test config data.
std::shared_ptr<ProjectContext> project = GetTestProject();
// Construct the Encoder.
Encoder encoder(project, ClientSecret::GenerateNewSecret());
// Set a static current time so we can test the day_index computation.
encoder.set_current_time(kSomeTimestamp);
// Encode an observation for the given metric and encoding. The metric is
// expected to have a single part.
Encoder::Result result =
encoder.EncodeString(metric_id, encoding_config_id, value);
CheckSinglePartResult(result, metric_id, encoding_config_id, expect_utc,
expected_encoding);
return *result.observation;
}
// Tests the EncodeInt() method using the given |value| and the given
// metric and encoding. The metric is expected to have a single part named
// "Part1". The encoding is expected to be for Basic RAPPOR.
// We validate that there are no errors and that the
// produced Observation has the |expected_type| and is non-empty.
// Returns the encoded Observation.
Observation DoEncodeIntTest(
int64_t value, uint32_t metric_id, uint32_t encoding_config_id,
bool expect_utc, const ObservationPart::ValueCase& expected_encoding) {
// Build the ProjectContext encapsulating our test config data.
std::shared_ptr<ProjectContext> project = GetTestProject();
// Construct the Encoder.
Encoder encoder(project, ClientSecret::GenerateNewSecret());
// Set a static current time so we can test the day_index computation.
encoder.set_current_time(kSomeTimestamp);
// Encode an observation for the given metric and encoding. The metric is
// expected to have a single part.
Encoder::Result result =
encoder.EncodeInt(metric_id, encoding_config_id, value);
CheckSinglePartResult(result, metric_id, encoding_config_id, expect_utc,
expected_encoding);
return *result.observation;
}
// Tests the EncodeIndex() method using the given |index| and the given
// metric and encoding. The metric is expected to have a single part named
// "Part1".
//
// If expectOK is true then we verify that there are no errors and that the
// produced Observation has the |expected_type| and is non-empty. Otherwise
// we verify that kInvalidArguments is returned.
void DoEncodeIndexTest(bool expectOK, uint32_t index, uint32_t metric_id,
uint32_t encoding_config_id, bool expect_utc,
const ObservationPart::ValueCase& expected_encoding) {
// Build the ProjectContext encapsulating our test config data.
std::shared_ptr<ProjectContext> project = GetTestProject();
// Construct the Encoder.
Encoder encoder(project, ClientSecret::GenerateNewSecret());
// Set a static current time so we can test the day_index computation.
encoder.set_current_time(kSomeTimestamp);
// Encode an observation for the given metric and encoding. The metric is
// expected to have a single part.
Encoder::Result result =
encoder.EncodeIndex(metric_id, encoding_config_id, index);
if (expectOK) {
CheckSinglePartResult(result, metric_id, encoding_config_id, expect_utc,
expected_encoding);
} else {
EXPECT_EQ(Encoder::kInvalidArguments, result.status)
<< "encoding_config_id=" << encoding_config_id;
}
}
// Tests the EncodeBlob() method using the given |value| and the given
// metric and encoding. The metric is expected to have a single part named
// "Part1". The encoding is expected to be for Forculus.
// We validate that there are no errors and that the
// produced Observation has the |expected_type| and is non-empty.
// Returns the encoded Observation.
Observation DoEncodeBlobTest(
const void* data, size_t num_bytes, uint32_t metric_id,
uint32_t encoding_config_id, bool expect_utc,
const ObservationPart::ValueCase& expected_encoding) {
// Build the ProjectContext encapsulating our test config data.
std::shared_ptr<ProjectContext> project = GetTestProject();
// Construct the Encoder.
Encoder encoder(project, ClientSecret::GenerateNewSecret());
// Set a static current time so we can test the day_index computation.
encoder.set_current_time(kSomeTimestamp);
// Encode an observation for the given metric and encoding. The metric is
// expected to have a single part.
Encoder::Result result =
encoder.EncodeBlob(metric_id, encoding_config_id, data, num_bytes);
CheckSinglePartResult(result, metric_id, encoding_config_id, expect_utc,
expected_encoding);
return *result.observation;
}
// Tests EncodeString() with Forculus as the specified encoding.
TEST(EncoderTest, EncodeStringForculus) {
// Metric 1 has a single string part.
// EncodingConfig 1 is Forculus.
DoEncodeStringTest("some value", 1, 1, false, ObservationPart::kForculus);
}
// Tests EncodeString() with String RAPPOR as the specified encoding.
TEST(EncoderTest, EncodeStringRappor) {
// Metric 1 has a single string part.
// EncodingConfig 2 is String RAPPOR
DoEncodeStringTest("some value", 1, 2, false, ObservationPart::kRappor);
}
// Tests EncodeString() with Basic RAPPOR as the specified encoding.
TEST(EncoderTest, EncodeStringBasicRappor) {
// Metric 1 has a single string part.
// EncodingConfig 3 is Basic RAPPOR with string values. Here we need the
// value to be one of the categories.
DoEncodeStringTest("Apple", 1, 3, false, ObservationPart::kBasicRappor);
}
// Tests EncodeString() with NoOp as the specified encoding.
TEST(EncoderTest, EncodeStringNoOp) {
// Metric 1 has a single string part.
// EncodingConfig 7 is NoOp.
auto obs = DoEncodeStringTest("some value", 1, 7, false,
ObservationPart::kUnencoded);
EXPECT_EQ(
"some value",
obs.parts().at("Part1").unencoded().unencoded_value().string_value());
}
// Tests EncodeInt() with Basic RAPPOR as the specified encoding.
TEST(EncoderTest, EncodeIntBasicRappor) {
// Metric 2 has a single integer part.
// EncodingConfig 4 is Basic RAPPOR with int values. Here we need the value
// to be one of the categories.
DoEncodeIntTest(125, 2, 4, true, ObservationPart::kBasicRappor);
}
// Tests the EncodeIndex() method with both valid and invalid inputs.
TEST(EncoderTest, EncodeIndex) {
// Metric 6 has a single part of type INDEX.
// EncodingConfig 8 is Basic RAPPOR with five INDEXed categories.
bool expect_ok = true;
uint32_t index = 0;
bool expect_utc = true;
DoEncodeIndexTest(expect_ok, index, 6, 8, expect_utc,
ObservationPart::kBasicRappor);
index = 1;
DoEncodeIndexTest(expect_ok, index, 6, 8, expect_utc,
ObservationPart::kBasicRappor);
index = 4;
DoEncodeIndexTest(expect_ok, index, 6, 8, expect_utc,
ObservationPart::kBasicRappor);
// Index 5 should yield kInalidArgs.
expect_ok = false;
index = 5;
DoEncodeIndexTest(expect_ok, index, 6, 8, expect_utc,
ObservationPart::kBasicRappor);
// Reset to index 0 just to confirm it still succeeds.
expect_ok = true;
index = 0;
DoEncodeIndexTest(expect_ok, index, 6, 8, expect_utc,
ObservationPart::kBasicRappor);
// Now we switch to metric 1 which has one string part. That should fail.
expect_ok = false;
DoEncodeIndexTest(expect_ok, index, 1, 8, expect_utc,
ObservationPart::kBasicRappor);
// Now we switch to metric 2 which has one int part. That should fail.
DoEncodeIndexTest(expect_ok, index, 2, 8, expect_utc,
ObservationPart::kBasicRappor);
// Now we switch to metric 3 which has one blob part. That should fail.
DoEncodeIndexTest(expect_ok, index, 3, 8, expect_utc,
ObservationPart::kBasicRappor);
// Reset to metric 6 just to confirm it still succeeds.
expect_ok = true;
DoEncodeIndexTest(expect_ok, index, 6, 8, expect_utc,
ObservationPart::kBasicRappor);
// Now we switch to encoding 1 which is Forculus. That should fail.
expect_ok = false;
DoEncodeIndexTest(expect_ok, index, 6, 1, expect_utc,
ObservationPart::kForculus);
// Now we switch to encoding 2 which is String RAPPOR. That should fail.
DoEncodeIndexTest(expect_ok, index, 6, 2, expect_utc,
ObservationPart::kRappor);
// Now we switch to encoding 3 which is Basic RAPPOR with string categories.
// That should fail.
DoEncodeIndexTest(expect_ok, index, 6, 3, expect_utc,
ObservationPart::kBasicRappor);
// Now we switch to encoding 4 which is Basic RAPPOR with int categories.
// That should fail.
DoEncodeIndexTest(expect_ok, index, 6, 4, expect_utc,
ObservationPart::kBasicRappor);
// Now we switch to encoding 7 which is NoOpEncoding. That should be OK.
expect_ok = true;
DoEncodeIndexTest(expect_ok, index, 6, 7, expect_utc,
ObservationPart::kUnencoded);
}
// Tests EncodeInt() with NoOp encoding as the specified encoding.
TEST(EncoderTest, EncodeIntNoOp) {
// Metric 2 has a single integer part.
// EncodingConfig 7 is NoOp
auto obs = DoEncodeIntTest(42, 2, 7, true, ObservationPart::kUnencoded);
EXPECT_EQ(42u,
obs.parts().at("Part1").unencoded().unencoded_value().int_value());
}
// Tests EncodeBlob() with Forculus as the specified encoding.
TEST(EncoderTest, EncodeBlobForculus) {
// Metric 3 has a single blob part.
// EncodingConfig 1 is Forculus.
std::string a_blob("This is a blob");
DoEncodeBlobTest((const void*)a_blob.data(), a_blob.size(), 3, 1, false,
ObservationPart::kForculus);
}
// Tests EncodeBlob() with NoOp encoding as the specified encoding.
TEST(EncoderTest, EncodeBlobNoOp) {
// Metric 3 has a single blob part.
// EncodingConfig 7 is NoOp.
std::string a_blob("This is a blob");
auto obs = DoEncodeBlobTest((const void*)a_blob.data(), a_blob.size(), 3, 7,
false, ObservationPart::kUnencoded);
EXPECT_EQ("This is a blob",
obs.parts().at("Part1").unencoded().unencoded_value().blob_value());
}
// Tests the advanced API, when used corretly.
TEST(EncoderTest, AdvancedApiNoErrors) {
// Build the ProjectContext encapsulating our test config data.
std::shared_ptr<ProjectContext> project = GetTestProject();
// Construct the Encoder.
Encoder encoder(project, ClientSecret::GenerateNewSecret());
Observation observation;
Encoder::Value value;
// EncodingConfig 2 is String RAPPOR
value.AddStringPart(2, "city", "San Francisco");
// EncodingConfig 4 is Basic RAPPOR with integer categories.
value.AddIntPart(4, "rating", 125);
// Metric 4 has a "city" part of type STRING and
// a "rating" part of type INT.
Encoder::Result result = encoder.Encode(4, value);
// Check the result
ASSERT_EQ(Encoder::kOK, result.status);
ASSERT_NE(nullptr, result.observation);
ASSERT_NE(nullptr, result.metadata);
EXPECT_EQ(kCustomerId, result.metadata->customer_id());
EXPECT_EQ(kProjectId, result.metadata->project_id());
EXPECT_EQ(4u, result.metadata->metric_id());
// We did not set the current time to a static value but rather used the
// real time that the test was run. Sanity test the day index: It should
// be at least the day on which this test was written and less than
// 20 years in the future from that.
EXPECT_TRUE(result.metadata->day_index() >= kPacificDayIndex);
EXPECT_TRUE(result.metadata->day_index() < kPacificDayIndex + 365 * 20);
EXPECT_NE("", result.observation->parts().at("city").rappor().data());
EXPECT_NE("", result.observation->parts().at("rating").basic_rappor().data());
}
// Tests the advanced API, when used incorretly.
TEST(EncoderTest, AdvancedApiWithErrors) {
// Build the ProjectContext encapsulating our test config data.
std::shared_ptr<ProjectContext> project = GetTestProject();
// Construct the Encoder.
Encoder encoder(project, ClientSecret::GenerateNewSecret());
std::unique_ptr<Encoder::Value> value(new Encoder::Value());
// EncodingConfig 2 is String RAPPOR
value->AddStringPart(2, "city", "San Francisco");
// There is no metric 99.
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(99, *value).status);
// Metric 4 has two parts but value has only one part.
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(4, *value).status);
// EncodingConfig 4 is Basic RAPPOR with integer categories.
value->AddIntPart(4, "rating", 1234);
value->AddIntPart(4, "dummy", 1234);
// Metric 4 has two parts but value has three parts.
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(4, *value).status);
value.reset(new Encoder::Value());
value->AddStringPart(2, "city", "San Francisco");
// "rating" is spelled wrong
value->AddIntPart(4, "ratingx", 1234);
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(4, *value).status);
value.reset(new Encoder::Value());
value->AddStringPart(2, "city", "San Francisco");
// "rating" has the wrong type
value->AddStringPart(4, "rating", "1234");
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(4, *value).status);
value.reset(new Encoder::Value());
value->AddStringPart(2, "city", "San Francisco");
// There is no encoding_config 99.
value->AddIntPart(99, "rating", 1234);
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(4, *value).status);
// Forculus does not accept integer values.
value.reset(new Encoder::Value());
value->AddIntPart(1, "Part1", 42);
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(2, *value).status);
// String RAPPOR does not accept integer values.
value.reset(new Encoder::Value());
value->AddIntPart(2, "Part1", 42);
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(2, *value).status);
// String RAPPOR does not accept blob values.
value.reset(new Encoder::Value());
value->AddBlobPart(2, "Part1", (const void*)"1234", 4);
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(3, *value).status);
// Basic RAPPOR does not accept blob values.
value.reset(new Encoder::Value());
value->AddBlobPart(3, "Part1", (const void*)"1234", 4);
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(3, *value).status);
// Basic RAPPOR requires the value to be one of the candidates.
value.reset(new Encoder::Value());
value->AddStringPart(3, "Part1", "San Francisco");
EXPECT_EQ(Encoder::kInvalidArguments, encoder.Encode(1, *value).status);
// EncodingConfig 5 is an invalid Forculus config.
value.reset(new Encoder::Value());
value->AddStringPart(5, "Part1", "dummy");
EXPECT_EQ(Encoder::kInvalidConfig, encoder.Encode(1, *value).status);
// EncodingConfig 6 is an invalid String RAPPOR config.
value.reset(new Encoder::Value());
value->AddStringPart(6, "Part1", "dummy");
EXPECT_EQ(Encoder::kInvalidConfig, encoder.Encode(1, *value).status);
// Metric 5 is missing a time_zone_policy.
value.reset(new Encoder::Value());
value->AddStringPart(1, "Part1", "dummy");
EXPECT_EQ(Encoder::kInvalidConfig, encoder.Encode(5, *value).status);
}
} // namespace encoder
} // namespace cobalt
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
google::ParseCommandLineFlags(&argc, &argv, true);
google::InitGoogleLogging(argv[0]);
google::InstallFailureSignalHandler();
return RUN_ALL_TESTS();
}