From 044fe455de76fac481759b3f621ea739df509d91 Mon Sep 17 00:00:00 2001 From: Tristan Vuong Date: Wed, 19 Jul 2023 15:58:19 +0000 Subject: [PATCH 1/5] Add reporting v2 cli tool. --- .../service/api/v2alpha/tools/BUILD.bazel | 29 + .../service/api/v2alpha/tools/README.md | 163 ++++++ .../service/api/v2alpha/tools/Reporting.kt | 511 +++++++++++++++++ .../service/api/v2alpha/tools/BUILD.bazel | 32 ++ .../api/v2alpha/tools/ReportingTest.kt | 535 ++++++++++++++++++ .../tools/reporting_metric_entry.textproto | 19 + .../v2alpha/tools/set_expression.textproto | 6 + 7 files changed, 1295 insertions(+) create mode 100644 src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/BUILD.bazel create mode 100644 src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/README.md create mode 100644 src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt create mode 100644 src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/BUILD.bazel create mode 100644 src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt create mode 100644 src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/reporting_metric_entry.textproto create mode 100644 src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/set_expression.textproto diff --git a/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/BUILD.bazel b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/BUILD.bazel new file mode 100644 index 00000000000..096f8a4882e --- /dev/null +++ b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/BUILD.bazel @@ -0,0 +1,29 @@ +load("@rules_java//java:defs.bzl", "java_binary") +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +package( + default_visibility = ["//src/test/kotlin/org/wfanet/measurement/reporting:__subpackages__"], +) + +kt_jvm_library( + name = "reporting", + srcs = ["Reporting.kt"], + deps = [ + "//src/main/kotlin/org/wfanet/measurement/kingdom/deploy/common:flags", + "//src/main/proto/wfa/measurement/reporting/v2alpha:event_groups_service_kt_jvm_grpc_proto", + "//src/main/proto/wfa/measurement/reporting/v2alpha:reporting_sets_service_kt_jvm_grpc_proto", + "//src/main/proto/wfa/measurement/reporting/v2alpha:reports_service_kt_jvm_grpc_proto", + "@wfa_common_jvm//imports/java/picocli", + "@wfa_common_jvm//imports/kotlin/com/google/protobuf/kotlin", + "@wfa_common_jvm//imports/kotlin/kotlinx/coroutines:core", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/grpc", + ], +) + +java_binary( + name = "Reporting", + main_class = "org.wfanet.measurement.reporting.service.api.v2alpha.tools.ReportingKt", + tags = ["manual"], + runtime_deps = [":reporting"], +) diff --git a/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/README.md b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/README.md new file mode 100644 index 00000000000..0e410cb4df1 --- /dev/null +++ b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/README.md @@ -0,0 +1,163 @@ +# Reporting CLI Tools + +Command-line tools for Reporting API. Use the `help` subcommand for help with +any of the subcommands. + +Note that instead of specifying arguments on the command-line, you can specify +an arguments file using `@` followed by the path. For example, + +```shell +Reporting @/home/foo/args.txt +``` + +## Certificate Host + +In the event that the host you specify to the `--reporting-server-api-target` +option doesn't match what's in the Subject Alternative Name (SAN) extension of +the server's certificate, you'll need to specify a host that does match using +the `--reporting-server-api-cert-host` option. + +## Examples + +### reporting-sets + +#### create + +```shell +Reporting \ + --tls-cert-file=src/main/k8s/testing/secretfiles/mc_tls.pem \ + --tls-key-file=src/main/k8s/testing/secretfiles/mc_tls.key \ + --cert-collection-file src/main/k8s/testing/secretfiles/reporting_root.pem \ + --reporting-server-api-target=v2alpha.reporting.dev.halo-cmm.org:8443 \ + reporting-sets create \ + --parent=measurementConsumers/VCTqwV_vFXw \ + --cmms-event-group=dataProviders/FeQ5FqAQ5_0/eventGroups/OsICFAbXJ5c \ + --filter='video_ad.age.value == 1' --display-name='test-reporting-set' \ + --id=abc +``` + +```shell +Reporting \ + --tls-cert-file=src/main/k8s/testing/secretfiles/mc_tls.pem \ + --tls-key-file=src/main/k8s/testing/secretfiles/mc_tls.key \ + --cert-collection-file src/main/k8s/testing/secretfiles/reporting_root.pem \ + --reporting-server-api-target=v2alpha.reporting.dev.halo-cmm.org:8443 \ + reporting-sets create \ + --parent=measurementConsumers/VCTqwV_vFXw \ + --set-expression=' + operation: UNION + lhs { + reporting_set: "measurementConsumers/VCTqwV_vFXw/reportingSets/abc" + } + ' \ + --filter='video_ad.age.value == 1' --display-name='test-reporting-set' \ + --id=abc +``` + +User specifies a primitive ReportingSet with one or more `--cmms-event-group` +and a composite ReportingSet with `--set-expression`. + +The `--set-expression` option expects a +[`SetExpression`](../../../../../../../../../proto/wfa/measurement/reporting/v2alpha/reporting_set.proto) +protobuf message in text format. You can use shell quoting for a multiline string, or +use command substitution to read the message from a file e.g. `--set-expression=$(cat +set_expression.textproto)`. + +#### list + +```shell +Reporting \ + --tls-cert-file=src/main/k8s/testing/secretfiles/mc_tls.pem \ + --tls-key-file=src/main/k8s/testing/secretfiles/mc_tls.key \ + --cert-collection-file src/main/k8s/testing/secretfiles/reporting_root.pem \ + --reporting-server-api-target v2alpha.reporting.dev.halo-cmm.org:8443 \ + reporting-sets list --parent=measurementConsumers/VCTqwV_vFXw +``` + +To retrieve the next page of reports, use the `--page-token` option to specify +the token returned from the previous response. + +### reports + +#### create + +```shell +Reporting \ + --tls-cert-file=secretfiles/mc_tls.pem \ + --tls-key-file=secretfiles/mc_tls.key \ + --cert-collection-file=secretfiles/reporting_root.pem \ + --reporting-server-api-target=v2alpha.reporting.dev.halo-cmm.org:8443 \ + reports create \ + --parent=measurementConsumers/VCTqwV_vFXw \ + --id=abcd \ + --request-id=abcd \ + --interval-start-time=2023-01-15T01:30:15.01Z \ + --interval-end-time=2023-06-27T23:19:12.99Z \ + --interval-start-time=2023-06-28T09:48:35.57Z \ + --interval-end-time=2023-12-13T11:57:54.21Z \ + --reporting-metric-entry=' + key: "measurementConsumers/VCTqwV_vFXw/reportingSets/abc" + value { + metric_calculation_specs { + display_name: "spec_1" + metric_specs { + reach { + privacy_params { + epsilon: 0.0041 + delta: 1.0E-12 + } + } + vid_sampling_interval { + width: 0.01 + } + } + } + } + ' +``` + +User specifies the type of time args by using either repeated interval params( +`--interval-start-time`, `--interval-end-time`) or periodic time args( +`--periodic-interval-start-time`, `--periodic-interval-increment` and +`--periodic-interval-count`) + +The `--reporting-metric-entry` option expects a +[`ReportingMetricEntry`](../../../../../../../../../proto/wfa/measurement/reporting/v2alpha/report.proto) +protobuf message in text format. You can use shell quoting for a multiline string, or +use command substitution to read the message from a file e.g. `--reporting-metric-entry=$(cat +reporting_metric_entry.textproto)`. + +#### list + +```shell +Reporting \ + --tls-cert-file=secretfiles/mc_tls.pem \ + --tls-key-file=secretfiles/mc_tls.key \ + --cert-collection-file=secretfiles/reporting_root.pem \ + --reporting-server-api-target=v2alpha.reporting.dev.halo-cmm.org:8443 \ + reports list --parent=measurementConsumers/VCTqwV_vFXw +``` + +#### get + +```shell +Reporting \ + --tls-cert-file=secretfiles/mc_tls.pem \ + --tls-key-file=secretfiles/mc_tls.key \ + --cert-collection-file=secretfiles/reporting_root.pem \ + --reporting-server-api-target=v2alpha.reporting.dev.halo-cmm.org:8443 \ + reports get measurementConsumers/VCTqwV_vFXw/reports/abcd +``` + +### event-groups + +#### list +```shell +Reporting \ + --tls-cert-file=secretfiles/mc_tls.pem \ + --tls-key-file=secretfiles/mc_tls.key \ + --cert-collection-file=secretfiles/reporting_root.pem \ + --reporting-server-api-target=v2alpha.reporting.dev.halo-cmm.org:8443 \ + event-groups list \ + --parent=measurementConsumers/VCTqwV_vFXw +``` diff --git a/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt new file mode 100644 index 00000000000..a68b0280453 --- /dev/null +++ b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt @@ -0,0 +1,511 @@ +/* + * Copyright 2023 The Cross-Media Measurement 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. + */ + +package org.wfanet.measurement.reporting.service.api.v2alpha.tools + +import com.google.type.interval +import io.grpc.ManagedChannel +import java.time.Duration +import java.time.Instant +import kotlin.properties.Delegates +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.wfanet.measurement.common.DurationFormat +import org.wfanet.measurement.common.commandLineMain +import org.wfanet.measurement.common.crypto.SigningCerts +import org.wfanet.measurement.common.grpc.TlsFlags +import org.wfanet.measurement.common.grpc.buildMutualTlsChannel +import org.wfanet.measurement.common.grpc.withShutdownTimeout +import org.wfanet.measurement.common.parseTextProto +import org.wfanet.measurement.common.toProtoDuration +import org.wfanet.measurement.common.toProtoTime +import org.wfanet.measurement.reporting.v2alpha.EventGroupsGrpcKt.EventGroupsCoroutineStub +import org.wfanet.measurement.reporting.v2alpha.Report +import org.wfanet.measurement.reporting.v2alpha.ReportingSet +import org.wfanet.measurement.reporting.v2alpha.ReportingSetKt +import org.wfanet.measurement.reporting.v2alpha.ReportingSetsGrpcKt.ReportingSetsCoroutineStub +import org.wfanet.measurement.reporting.v2alpha.ReportsGrpcKt.ReportsCoroutineStub +import org.wfanet.measurement.reporting.v2alpha.createReportRequest +import org.wfanet.measurement.reporting.v2alpha.createReportingSetRequest +import org.wfanet.measurement.reporting.v2alpha.getReportRequest +import org.wfanet.measurement.reporting.v2alpha.listEventGroupsRequest +import org.wfanet.measurement.reporting.v2alpha.listReportingSetsRequest +import org.wfanet.measurement.reporting.v2alpha.listReportsRequest +import org.wfanet.measurement.reporting.v2alpha.periodicTimeInterval +import org.wfanet.measurement.reporting.v2alpha.report +import org.wfanet.measurement.reporting.v2alpha.reportingSet +import org.wfanet.measurement.reporting.v2alpha.timeIntervals +import picocli.CommandLine + +private class ReportingApiFlags { + @CommandLine.Option( + names = ["--reporting-server-api-target"], + description = ["gRPC target (authority) of the reporting server's public API"], + required = true, + ) + lateinit var apiTarget: String + private set + + @CommandLine.Option( + names = ["--reporting-server-api-cert-host"], + description = + [ + "Expected hostname (DNS-ID) in the reporting server's TLS certificate.", + "This overrides derivation of the TLS DNS-ID from --reporting-server-api-target.", + ], + required = false, + ) + var apiCertHost: String? = null + private set +} + +private class PageParams { + @CommandLine.Option( + names = ["--page-size"], + description = ["The maximum number of items to return. The maximum value is 1000"], + required = false, + ) + var pageSize: Int = 1000 + private set + + @CommandLine.Option( + names = ["--page-token"], + description = ["Page token from a previous list call to retrieve the next page"], + defaultValue = "", + required = false, + ) + lateinit var pageToken: String + private set +} + +@CommandLine.Command(name = "create", description = ["Creates a reporting set"]) +class CreateReportingSetCommand : Runnable { + @CommandLine.ParentCommand private lateinit var parent: ReportingSetsCommand + + @CommandLine.Option( + names = ["--parent"], + description = ["API resource name of the Measurement Consumer"], + required = true, + ) + private lateinit var measurementConsumerName: String + + class ReportingSetType { + @CommandLine.Option( + names = ["--cmms-event-group"], + description = ["List of CMMS EventGroup resource names"], + required = false, + ) + var cmmsEventGroups: List? = null + + @CommandLine.Option( + names = ["--set-expression"], + description = ["SetExpression protobuf messages in text format"], + required = false, + ) + var textFormatSetExpression: String? = null + } + + @CommandLine.ArgGroup(exclusive = true, multiplicity = "1", heading = "Reporting Set Type\n") + private lateinit var type: ReportingSetType + + @CommandLine.Option( + names = ["--filter"], + description = ["CEL filter predicate that applies to all `event_groups`"], + required = false, + defaultValue = "" + ) + private lateinit var filterExpression: String + + @CommandLine.Option( + names = ["--display-name"], + description = ["Human-readable name for display purposes"], + required = false, + defaultValue = "" + ) + private lateinit var displayNameInput: String + + @CommandLine.Option( + names = ["--id"], + description = ["Resource ID of the Reporting Set"], + required = true, + defaultValue = "" + ) + private lateinit var reportingSetId: String + + override fun run() { + val request = createReportingSetRequest { + parent = measurementConsumerName + reportingSet = reportingSet { + if (type.cmmsEventGroups != null && type.cmmsEventGroups!!.isNotEmpty()) { + primitive = ReportingSetKt.primitive { + type.cmmsEventGroups!!.forEach { + cmmsEventGroups += it + } + } + } else if (type.textFormatSetExpression != null) { + composite = ReportingSetKt.composite { + expression = parseTextProto(type.textFormatSetExpression!!.reader(), ReportingSet.SetExpression.getDefaultInstance()) + } + } + filter = filterExpression + displayName = displayNameInput + } + reportingSetId = this@CreateReportingSetCommand.reportingSetId + } + val reportingSet = + runBlocking(Dispatchers.IO) { parent.reportingSetStub.createReportingSet(request) } + println(reportingSet) + } +} + +@CommandLine.Command(name = "list", description = ["List reporting sets"]) +class ListReportingSetsCommand : Runnable { + @CommandLine.ParentCommand private lateinit var parent: ReportingSetsCommand + + @CommandLine.Option( + names = ["--parent"], + description = ["API resource name of the Measurement Consumer"], + required = true, + ) + private lateinit var measurementConsumerName: String + + @CommandLine.Mixin private lateinit var pageParams: PageParams + + override fun run() { + val request = listReportingSetsRequest { + parent = measurementConsumerName + pageSize = pageParams.pageSize + pageToken = pageParams.pageToken + } + + val response = + runBlocking(Dispatchers.IO) { parent.reportingSetStub.listReportingSets(request) } + + println(response) + } +} + +@CommandLine.Command( + name = "reporting-sets", + sortOptions = false, + subcommands = + [ + CommandLine.HelpCommand::class, + CreateReportingSetCommand::class, + ListReportingSetsCommand::class, + ] +) +class ReportingSetsCommand : Runnable { + @CommandLine.ParentCommand lateinit var parent: Reporting + val reportingSetStub: ReportingSetsCoroutineStub by lazy { + ReportingSetsCoroutineStub(parent.channel) + } + override fun run() {} +} + +@CommandLine.Command(name = "create", description = ["Create a report"]) +class CreateReportCommand : Runnable { + @CommandLine.ParentCommand private lateinit var parent: ReportsCommand + + @CommandLine.Option( + names = ["--parent"], + description = ["API resource name of the Measurement Consumer"], + required = true, + ) + private lateinit var measurementConsumerName: String + + @CommandLine.Option( + names = ["--reporting-metric-entry"], + description = ["ReportingMetricEntry protobuf messages in text format"], + required = false, + ) + lateinit var textFormatReportingMetricEntries: List + + class TimeInput { + class TimeIntervalInput { + @CommandLine.Option( + names = ["--interval-start-time"], + description = ["Start of time interval in ISO 8601 format of UTC"], + required = true, + ) + lateinit var intervalStartTime: Instant + private set + + @CommandLine.Option( + names = ["--interval-end-time"], + description = ["End of time interval in ISO 8601 format of UTC"], + required = true, + ) + lateinit var intervalEndTime: Instant + private set + } + + class PeriodicTimeIntervalInput { + @CommandLine.Option( + names = ["--periodic-interval-start-time"], + description = ["Start of the first time interval in ISO 8601 format of UTC"], + required = true, + ) + lateinit var periodicIntervalStartTime: Instant + private set + + @CommandLine.Option( + names = ["--periodic-interval-increment"], + description = ["Increment for each time interval in ISO-8601 format of PnDTnHnMn"], + required = true + ) + lateinit var periodicIntervalIncrement: Duration + private set + + @set:CommandLine.Option( + names = ["--periodic-interval-count"], + description = ["Number of periodic intervals"], + required = true + ) + var periodicIntervalCount by Delegates.notNull() + private set + } + + @CommandLine.ArgGroup(exclusive = false, multiplicity = "1..*", heading = "Time intervals\n") + var timeIntervals: List? = null + private set + + @CommandLine.ArgGroup( + exclusive = false, + multiplicity = "1", + heading = "Periodic time interval specification\n" + ) + var periodicTimeIntervalInput: PeriodicTimeIntervalInput? = null + private set + } + + @CommandLine.ArgGroup( + exclusive = true, + multiplicity = "1", + heading = "Time interval or periodic time interval\n" + ) + private lateinit var timeInput: TimeInput + + @CommandLine.Option( + names = ["--id"], + description = ["Resource ID of the Report"], + required = true, + defaultValue = "" + ) + private lateinit var reportId: String + + @CommandLine.Option( + names = ["--request-id"], + description = ["Request ID for creation of Report"], + required = false, + defaultValue = "" + ) + private lateinit var requestId: String + + override fun run() { + val request = createReportRequest { + parent = measurementConsumerName + report = report { + for (textFormatReportingMetricEntry in textFormatReportingMetricEntries) { + reportingMetricEntries += parseTextProto(textFormatReportingMetricEntry.reader(), Report.ReportingMetricEntry.getDefaultInstance()) + } + + // Either timeIntervals or periodicTimeIntervalInput is set. + if (timeInput.timeIntervals != null) { + val intervals = checkNotNull(timeInput.timeIntervals) + timeIntervals = timeIntervals { + intervals.forEach { + timeIntervals += interval { + startTime = it.intervalStartTime.toProtoTime() + endTime = it.intervalEndTime.toProtoTime() + } + } + } + } else { + val periodicIntervals = checkNotNull(timeInput.periodicTimeIntervalInput) + periodicTimeInterval = periodicTimeInterval { + startTime = periodicIntervals.periodicIntervalStartTime.toProtoTime() + increment = periodicIntervals.periodicIntervalIncrement.toProtoDuration() + intervalCount = periodicIntervals.periodicIntervalCount + } + } + } + reportId = this@CreateReportCommand.reportId + requestId = this@CreateReportCommand.requestId + } + val report = runBlocking(Dispatchers.IO) { parent.reportsStub.createReport(request) } + + println(report) + } +} + +@CommandLine.Command(name = "list", description = ["List reports"]) +class ListReportsCommand : Runnable { + @CommandLine.ParentCommand private lateinit var parent: ReportsCommand + + @CommandLine.Option( + names = ["--parent"], + description = ["API resource name of the Measurement Consumer"], + required = true, + ) + private lateinit var measurementConsumerName: String + + @CommandLine.Mixin private lateinit var pageParams: PageParams + + override fun run() { + val request = listReportsRequest { + parent = measurementConsumerName + pageSize = pageParams.pageSize + pageToken = pageParams.pageToken + } + + val response = runBlocking(Dispatchers.IO) { parent.reportsStub.listReports(request) } + + response.reportsList.forEach { println(it.name + " " + it.state.toString()) } + if (response.nextPageToken.isNotEmpty()) { + println("nextPageToken: ${response.nextPageToken}") + } + } +} + +@CommandLine.Command(name = "get", description = ["Get a report"]) +class GetReportCommand : Runnable { + @CommandLine.ParentCommand private lateinit var parent: ReportsCommand + + @CommandLine.Parameters( + description = ["API resource name of the Report"], + ) + private lateinit var reportName: String + + override fun run() { + val request = getReportRequest { name = reportName } + + val report = runBlocking(Dispatchers.IO) { parent.reportsStub.getReport(request) } + println(report) + } +} + +@CommandLine.Command( + name = "reports", + sortOptions = false, + subcommands = + [ + CommandLine.HelpCommand::class, + CreateReportCommand::class, + ListReportsCommand::class, + GetReportCommand::class, + ] +) +class ReportsCommand : Runnable { + @CommandLine.ParentCommand lateinit var parent: Reporting + + val reportsStub: ReportsCoroutineStub by lazy { ReportsCoroutineStub(parent.channel) } + + override fun run() {} +} + +@CommandLine.Command(name = "list", description = ["List event groups"]) +class ListEventGroups : Runnable { + @CommandLine.ParentCommand private lateinit var parent: EventGroupsCommand + + @CommandLine.Option( + names = ["--parent"], + description = ["API resource name of the Measurement Consumer"], + required = true, + ) + private lateinit var measurementConsumerName: String + + @CommandLine.Option( + names = ["--filter"], + description = ["Result filter in format of raw CEL expression"], + required = false, + defaultValue = "" + ) + private lateinit var celFilter: String + + @CommandLine.Mixin private lateinit var pageParams: PageParams + + override fun run() { + val request = listEventGroupsRequest { + parent = measurementConsumerName + pageSize = pageParams.pageSize + pageToken = pageParams.pageToken + filter = celFilter + } + + val response = runBlocking(Dispatchers.IO) { parent.eventGroupStub.listEventGroups(request) } + + println(response) + } +} + +@CommandLine.Command( + name = "event-groups", + sortOptions = false, + subcommands = + [ + CommandLine.HelpCommand::class, + ListEventGroups::class, + ] +) +class EventGroupsCommand : Runnable { + @CommandLine.ParentCommand lateinit var parent: Reporting + + val eventGroupStub: EventGroupsCoroutineStub by lazy { EventGroupsCoroutineStub(parent.channel) } + + override fun run() {} +} + +@CommandLine.Command( + name = "reporting", + description = ["Reporting CLI tool"], + sortOptions = false, + subcommands = + [ + CommandLine.HelpCommand::class, + ReportingSetsCommand::class, + ReportsCommand::class, + EventGroupsCommand::class, + ] +) +class Reporting : Runnable { + @CommandLine.Mixin private lateinit var tlsFlags: TlsFlags + @CommandLine.Mixin private lateinit var apiFlags: ReportingApiFlags + + val channel: ManagedChannel by lazy { + val clientCerts = + SigningCerts.fromPemFiles( + certificateFile = tlsFlags.certFile, + privateKeyFile = tlsFlags.privateKeyFile, + trustedCertCollectionFile = tlsFlags.certCollectionFile + ) + buildMutualTlsChannel(apiFlags.apiTarget, clientCerts, apiFlags.apiCertHost) + .withShutdownTimeout(Duration.ofSeconds(1)) + } + override fun run() {} + + companion object { + @JvmStatic + fun main(args: Array) = commandLineMain(Reporting(), args, DurationFormat.ISO_8601) + } +} + +/** + * Reporting Set, Report, and Event Group methods. + * + * Use the `help` command to see usage details. + */ +fun main(args: Array) = commandLineMain(Reporting(), args) diff --git a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/BUILD.bazel b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/BUILD.bazel new file mode 100644 index 00000000000..d0442b5b08b --- /dev/null +++ b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/BUILD.bazel @@ -0,0 +1,32 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") + +filegroup( + name = "textproto_files", + srcs = glob(["*.textproto"]), +) + +kt_jvm_test( + name = "ReportingTest", + srcs = ["ReportingTest.kt"], + data = [ + "textproto_files", + "//src/main/k8s/testing/secretfiles:root_certs", + "//src/main/k8s/testing/secretfiles:secret_files", + ], + jvm_flags = ["-Dcom.google.testing.junit.runner.shouldInstallTestSecurityManager=false"], + test_class = "org.wfanet.measurement.reporting.service.api.v2alpha.tools.ReportingTest", + deps = [ + "//src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools:reporting", + "//src/main/proto/wfa/measurement/reporting/v2alpha:event_groups_service_kt_jvm_grpc_proto", + "//src/main/proto/wfa/measurement/reporting/v2alpha:reporting_sets_service_kt_jvm_grpc_proto", + "//src/main/proto/wfa/measurement/reporting/v2alpha:reports_service_kt_jvm_grpc_proto", + "@wfa_common_jvm//imports/java/com/google/common/truth", + "@wfa_common_jvm//imports/java/com/google/common/truth/extensions/proto", + "@wfa_common_jvm//imports/java/io/grpc/netty", + "@wfa_common_jvm//imports/java/org/junit", + "@wfa_common_jvm//imports/kotlin/kotlin/test", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/grpc/testing", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/testing", + ], +) diff --git a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt new file mode 100644 index 00000000000..753763e6bb5 --- /dev/null +++ b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt @@ -0,0 +1,535 @@ +/* + * Copyright 2023 The Cross-Media Measurement 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. + */ + +package org.wfanet.measurement.reporting.service.api.v2alpha.tools + +import com.google.common.truth.Truth.assertThat +import com.google.type.interval +import io.grpc.Server +import io.grpc.ServerServiceDefinition +import io.grpc.netty.NettyServerBuilder +import java.nio.file.Path +import java.nio.file.Paths +import java.time.Duration +import java.time.Instant +import java.util.concurrent.TimeUnit.SECONDS +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.kotlin.any +import org.wfanet.measurement.common.crypto.SigningCerts +import org.wfanet.measurement.common.getRuntimePath +import org.wfanet.measurement.common.grpc.testing.mockService +import org.wfanet.measurement.common.grpc.toServerTlsContext +import org.wfanet.measurement.common.parseTextProto +import org.wfanet.measurement.common.testing.CommandLineTesting +import org.wfanet.measurement.common.testing.verifyProtoArgument +import org.wfanet.measurement.common.toProtoDuration +import org.wfanet.measurement.common.toProtoTime +import org.wfanet.measurement.reporting.v2alpha.EventGroupsGrpcKt.EventGroupsCoroutineImplBase +import org.wfanet.measurement.reporting.v2alpha.ListEventGroupsResponse +import org.wfanet.measurement.reporting.v2alpha.ListReportingSetsResponse +import org.wfanet.measurement.reporting.v2alpha.Report +import org.wfanet.measurement.reporting.v2alpha.ReportingSet +import org.wfanet.measurement.reporting.v2alpha.ReportingSetKt +import org.wfanet.measurement.reporting.v2alpha.ReportingSetsGrpcKt.ReportingSetsCoroutineImplBase +import org.wfanet.measurement.reporting.v2alpha.ReportsGrpcKt.ReportsCoroutineImplBase +import org.wfanet.measurement.reporting.v2alpha.createReportRequest +import org.wfanet.measurement.reporting.v2alpha.createReportingSetRequest +import org.wfanet.measurement.reporting.v2alpha.eventGroup +import org.wfanet.measurement.reporting.v2alpha.getReportRequest +import org.wfanet.measurement.reporting.v2alpha.listEventGroupsRequest +import org.wfanet.measurement.reporting.v2alpha.listEventGroupsResponse +import org.wfanet.measurement.reporting.v2alpha.listReportingSetsRequest +import org.wfanet.measurement.reporting.v2alpha.listReportingSetsResponse +import org.wfanet.measurement.reporting.v2alpha.listReportsRequest +import org.wfanet.measurement.reporting.v2alpha.listReportsResponse +import org.wfanet.measurement.reporting.v2alpha.periodicTimeInterval +import org.wfanet.measurement.reporting.v2alpha.report +import org.wfanet.measurement.reporting.v2alpha.reportingSet +import org.wfanet.measurement.reporting.v2alpha.timeIntervals + +@RunWith(JUnit4::class) +class ReportingTest { + private val reportingSetsServiceMock: ReportingSetsCoroutineImplBase = + mockService { + onBlocking { createReportingSet(any()) }.thenReturn(REPORTING_SET) + onBlocking { listReportingSets(any()) }.thenReturn(listReportingSetsResponse { + reportingSets += REPORTING_SET + }) + } + private val reportsServiceMock: ReportsCoroutineImplBase = + mockService { + onBlocking { createReport(any()) }.thenReturn(REPORT) + onBlocking { listReports(any()) }.thenReturn(listReportsResponse { + reports += REPORT + }) + onBlocking { getReport(any()) }.thenReturn(REPORT) + } + private val eventGroupsServiceMock: EventGroupsCoroutineImplBase = + mockService { onBlocking { listEventGroups(any()) }.thenReturn(listEventGroupsResponse { + eventGroups += EVENT_GROUP + }) } + + private val serverCerts = + SigningCerts.fromPemFiles( + certificateFile = SECRETS_DIR.resolve("reporting_tls.pem").toFile(), + privateKeyFile = SECRETS_DIR.resolve("reporting_tls.key").toFile(), + trustedCertCollectionFile = SECRETS_DIR.resolve("reporting_root.pem").toFile(), + ) + + private val services: List = + listOf( + reportingSetsServiceMock.bindService(), + reportsServiceMock.bindService(), + eventGroupsServiceMock.bindService(), + ) + + private val server: Server = + NettyServerBuilder.forPort(0) + .sslContext(serverCerts.toServerTlsContext()) + .addServices(services) + .build() + + @Before + fun initServer() { + server.start() + } + + @After + fun shutdownServer() { + server.shutdown() + server.awaitTermination(1, SECONDS) + } + + private fun callCli(args: Array): String { + return CommandLineTesting.capturingSystemOut { + CommandLineTesting.assertExitsWith(0) { Reporting.main(args) } + } + } + + @Test + fun `create reporting set with --cmms-event-group calls api with valid request`() { + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reporting-sets", + "create", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--cmms-event-group=$CMMS_EVENT_GROUP_NAME_1", + "--cmms-event-group=$CMMS_EVENT_GROUP_NAME_2", + "--filter=person.age_group == 1", + "--display-name=reporting-set", + "--id=$REPORTING_SET_ID", + ) + + val output = callCli(args) + + verifyProtoArgument( + reportingSetsServiceMock, + ReportingSetsCoroutineImplBase::createReportingSet + ) + .isEqualTo( + createReportingSetRequest { + parent = MEASUREMENT_CONSUMER_NAME + reportingSet = reportingSet { + filter = "person.age_group == 1" + displayName = "reporting-set" + primitive = ReportingSetKt.primitive { + cmmsEventGroups += CMMS_EVENT_GROUP_NAME_1 + cmmsEventGroups += CMMS_EVENT_GROUP_NAME_2 + } + } + reportingSetId = REPORTING_SET_ID + } + ) + + assertThat(parseTextProto(output.reader(), ReportingSet.getDefaultInstance())) + .isEqualTo(REPORTING_SET) + } + + @Test + fun `create reporting set with --set-expression calls api with valid request`() { + val setExpression = + """ + operation: UNION + lhs { + reporting_set: "$REPORTING_SET_NAME" + } + """.trimIndent() + + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reporting-sets", + "create", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--set-expression=$setExpression", + "--filter=person.age_group == 1", + "--display-name=reporting-set", + "--id=$REPORTING_SET_ID", + ) + + val output = callCli(args) + + verifyProtoArgument( + reportingSetsServiceMock, + ReportingSetsCoroutineImplBase::createReportingSet + ) + .isEqualTo( + createReportingSetRequest { + parent = MEASUREMENT_CONSUMER_NAME + reportingSet = reportingSet { + filter = "person.age_group == 1" + displayName = "reporting-set" + composite = ReportingSetKt.composite { + expression = ReportingSetKt.setExpression { + operation = ReportingSet.SetExpression.Operation.UNION + lhs = ReportingSetKt.SetExpressionKt.operand { + reportingSet = REPORTING_SET_NAME + } + } + } + } + reportingSetId = REPORTING_SET_ID + } + ) + + assertThat(parseTextProto(output.reader(), ReportingSet.getDefaultInstance())) + .isEqualTo(REPORTING_SET) + } + + @Test + fun `create reporting set with both --set-expression and --cmms-event-groups fails`() { + val setExpression = + """ + operation: UNION + lhs { + reporting_set: "$REPORTING_SET_NAME" + } + """.trimIndent() + + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reporting-sets", + "create", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--cmms-event-group=$CMMS_EVENT_GROUP_NAME_1", + "--cmms-event-group=$CMMS_EVENT_GROUP_NAME_2", + "--set-expression=$setExpression", + "--filter=person.age_group == 1", + "--display-name=reporting-set", + "--id=$REPORTING_SET_ID", + ) + + CommandLineTesting.assertExitsWith(2) { Reporting.main(args) } + } + + @Test + fun `list reporting sets calls api with valid request`() { + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reporting-sets", + "list", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--page-size=50", + "--page-token=token", + ) + + val output = callCli(args) + + verifyProtoArgument(reportingSetsServiceMock, ReportingSetsCoroutineImplBase::listReportingSets) + .isEqualTo( + listReportingSetsRequest { + parent = MEASUREMENT_CONSUMER_NAME + pageSize = 50 + pageToken = "token" + } + ) + assertThat(parseTextProto(output.reader(), ListReportingSetsResponse.getDefaultInstance())) + .isEqualTo(listReportingSetsResponse { + reportingSets += REPORTING_SET + }) + } + + @Test + fun `create report with timeIntervalInput calls api with valid request`() { + val textFormatReportingMetricEntryFile = TEXTPROTO_DIR.resolve("reporting_metric_entry.textproto").toFile() + val startTime = "2017-01-15T01:30:15.01Z" + val endTime = "2018-01-15T01:30:15.01Z" + val startTime2 = "2019-01-15T01:30:15.01Z" + val endTime2 = "2020-01-15T01:30:15.01Z" + + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reports", + "create", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--interval-start-time=$startTime", + "--interval-end-time=$endTime", + "--interval-start-time=$startTime2", + "--interval-end-time=$endTime2", + "--id=$REPORT_ID", + "--request-id=$REPORT_REQUEST_ID", + "--reporting-metric-entry=${textFormatReportingMetricEntryFile.readText()}", + ) + + val output = callCli(args) + + verifyProtoArgument(reportsServiceMock, ReportsCoroutineImplBase::createReport) + .isEqualTo( + createReportRequest { + parent = MEASUREMENT_CONSUMER_NAME + reportId = REPORT_ID + requestId = REPORT_REQUEST_ID + report = report { + reportingMetricEntries += parseTextProto(textFormatReportingMetricEntryFile, Report.ReportingMetricEntry.getDefaultInstance()) + timeIntervals = timeIntervals { + timeIntervals += interval { + this.startTime = Instant.parse(startTime).toProtoTime() + this.endTime = Instant.parse(endTime).toProtoTime() + } + timeIntervals += interval { + this.startTime = Instant.parse(startTime2).toProtoTime() + this.endTime = Instant.parse(endTime2).toProtoTime() + } + } + } + } + ) + + assertThat(parseTextProto(output.reader(), Report.getDefaultInstance())).isEqualTo(REPORT) + } + + @Test + fun `create report with periodicTimeIntervalInput calls api with valid request`() { + val textFormatReportingMetricEntryFile = TEXTPROTO_DIR.resolve("reporting_metric_entry.textproto").toFile() + val startTime = "2017-01-15T01:30:15.01Z" + val increment = "P1DT3H5M12.99S" + + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reports", + "create", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--periodic-interval-start-time=$startTime", + "--periodic-interval-increment=$increment", + "--periodic-interval-count=3", + "--id=$REPORT_ID", + "--request-id=$REPORT_REQUEST_ID", + "--reporting-metric-entry=${textFormatReportingMetricEntryFile.readText()}", + ) + + val output = callCli(args) + + verifyProtoArgument(reportsServiceMock, ReportsCoroutineImplBase::createReport) + .isEqualTo( + createReportRequest { + parent = MEASUREMENT_CONSUMER_NAME + reportId = REPORT_ID + requestId = REPORT_REQUEST_ID + report = report { + reportingMetricEntries += parseTextProto(textFormatReportingMetricEntryFile, Report.ReportingMetricEntry.getDefaultInstance()) + periodicTimeInterval = periodicTimeInterval { + this.startTime = Instant.parse(startTime).toProtoTime() + this.increment = Duration.parse(increment).toProtoDuration() + intervalCount = 3 + } + } + } + ) + + assertThat(parseTextProto(output.reader(), Report.getDefaultInstance())).isEqualTo(REPORT) + } + + @Test + fun `create report with both periodicTimeIntervalInput and timeIntervalInput fails`() { + val textFormatReportingMetricEntryFile = TEXTPROTO_DIR.resolve("reporting_metric_entry.textproto").toFile() + val increment = "P1DT3H5M12.99S" + val startTime = "2017-01-15T01:30:15.01Z" + val endTime = "2018-01-15T01:30:15.01Z" + val startTime2 = "2019-01-15T01:30:15.01Z" + val endTime2 = "2020-01-15T01:30:15.01Z" + + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reports", + "create", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--periodic-interval-start-time=$startTime", + "--periodic-interval-increment=$increment", + "--periodic-interval-count=3", + "--interval-start-time=$startTime", + "--interval-end-time=$endTime", + "--interval-start-time=$startTime2", + "--interval-end-time=$endTime2", + "--id=$REPORT_ID", + "--request-id=$REPORT_REQUEST_ID", + "--reporting-metric-entry=${textFormatReportingMetricEntryFile.readText()}", + ) + + CommandLineTesting.assertExitsWith(2) { Reporting.main(args) } + } + + @Test + fun `list reports calls api with valid request`() { + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reports", + "list", + "--parent=$MEASUREMENT_CONSUMER_NAME", + ) + callCli(args) + + verifyProtoArgument(reportsServiceMock, ReportsCoroutineImplBase::listReports) + .isEqualTo( + listReportsRequest { + parent = MEASUREMENT_CONSUMER_NAME + pageSize = 1000 + } + ) + } + + @Test + fun `get report calls api with valid request`() { + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reports", + "get", + REPORT_NAME, + ) + val output = callCli(args) + + verifyProtoArgument(reportsServiceMock, ReportsCoroutineImplBase::getReport) + .isEqualTo(getReportRequest { name = REPORT_NAME }) + assertThat(parseTextProto(output.reader(), Report.getDefaultInstance())).isEqualTo(REPORT) + } + + @Test + fun `list event groups calls api with valid request`() { + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "event-groups", + "list", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--filter=event_group_reference_id == 'abc'", + ) + val output = callCli(args) + + verifyProtoArgument(eventGroupsServiceMock, EventGroupsCoroutineImplBase::listEventGroups) + .isEqualTo( + listEventGroupsRequest { + parent = MEASUREMENT_CONSUMER_NAME + filter = "event_group_reference_id == 'abc'" + pageSize = 1000 + } + ) + assertThat(parseTextProto(output.reader(), ListEventGroupsResponse.getDefaultInstance())) + .isEqualTo(listEventGroupsResponse { + eventGroups += EVENT_GROUP + }) + } + + companion object { + private const val HOST = "localhost" + private val SECRETS_DIR: Path = + getRuntimePath( + Paths.get("wfa_measurement_system", "src", "main", "k8s", "testing", "secretfiles") + )!! + + private val TEXTPROTO_DIR: Path = + getRuntimePath( + Paths.get( + "wfa_measurement_system", + "src", + "test", + "kotlin", + "org", + "wfanet", + "measurement", + "reporting", + "service", + "api", + "v2alpha", + "tools" + ) + )!! + + private const val MEASUREMENT_CONSUMER_NAME = "measurementConsumers/1" + private const val DATA_PROVIDER_NAME = "dataProviders/1" + private const val CMMS_EVENT_GROUP_NAME_1 = "$DATA_PROVIDER_NAME/eventGroups/1" + private const val CMMS_EVENT_GROUP_NAME_2 = "$DATA_PROVIDER_NAME/eventGroups/2" + + private const val REPORTING_SET_ID = "abc" + private const val REPORTING_SET_NAME = "reportingSet/$REPORTING_SET_ID" + + private val REPORTING_SET = reportingSet { + name = REPORTING_SET_NAME + } + + private const val REPORT_REQUEST_ID = "def" + private const val REPORT_ID = "abc" + private const val REPORT_NAME = "$MEASUREMENT_CONSUMER_NAME/reports/$REPORT_ID" + private val REPORT = report { + name = REPORT_NAME + } + + private const val EVENT_GROUP_NAME = "$MEASUREMENT_CONSUMER_NAME/eventGroups/1" + private val EVENT_GROUP = eventGroup { + name = EVENT_GROUP_NAME + } + } +} diff --git a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/reporting_metric_entry.textproto b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/reporting_metric_entry.textproto new file mode 100644 index 00000000000..3d57c279386 --- /dev/null +++ b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/reporting_metric_entry.textproto @@ -0,0 +1,19 @@ +# proto-file: wfa/measurement/reporting/v2alpha/report.proto +# proto-message: Report.ReportingMetricEntry +key: "measurementConsumers/Dipo47pr5to/reportingSets/abc" +value { + metric_calculation_specs { + display_name: "spec_1" + metric_specs { + reach { + privacy_params { + epsilon: 0.0041 + delta: 1.0E-12 + } + } + vid_sampling_interval { + width: 0.01 + } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/set_expression.textproto b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/set_expression.textproto new file mode 100644 index 00000000000..b374d01f80b --- /dev/null +++ b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/set_expression.textproto @@ -0,0 +1,6 @@ +# proto-file: wfa/measurement/reporting/v2alpha/reporting_set.proto +# proto-message: ReportingSet.SetExpression +operation: UNION +lhs { + reporting_set: "measurementConsumers/1/reportingSets/abc" +} From 82087dafd4f59a6a843db5f452834ada29264d84 Mon Sep 17 00:00:00 2001 From: Tristan Vuong Date: Wed, 19 Jul 2023 16:06:32 +0000 Subject: [PATCH 2/5] lint fix --- .../service/api/v2alpha/tools/Reporting.kt | 24 ++-- .../api/v2alpha/tools/ReportingTest.kt | 110 +++++++++--------- 2 files changed, 70 insertions(+), 64 deletions(-) diff --git a/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt index a68b0280453..bff1fc02647 100644 --- a/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt +++ b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt @@ -150,15 +150,17 @@ class CreateReportingSetCommand : Runnable { parent = measurementConsumerName reportingSet = reportingSet { if (type.cmmsEventGroups != null && type.cmmsEventGroups!!.isNotEmpty()) { - primitive = ReportingSetKt.primitive { - type.cmmsEventGroups!!.forEach { - cmmsEventGroups += it - } - } + primitive = + ReportingSetKt.primitive { type.cmmsEventGroups!!.forEach { cmmsEventGroups += it } } } else if (type.textFormatSetExpression != null) { - composite = ReportingSetKt.composite { - expression = parseTextProto(type.textFormatSetExpression!!.reader(), ReportingSet.SetExpression.getDefaultInstance()) - } + composite = + ReportingSetKt.composite { + expression = + parseTextProto( + type.textFormatSetExpression!!.reader(), + ReportingSet.SetExpression.getDefaultInstance() + ) + } } filter = filterExpression displayName = displayNameInput @@ -320,7 +322,11 @@ class CreateReportCommand : Runnable { parent = measurementConsumerName report = report { for (textFormatReportingMetricEntry in textFormatReportingMetricEntries) { - reportingMetricEntries += parseTextProto(textFormatReportingMetricEntry.reader(), Report.ReportingMetricEntry.getDefaultInstance()) + reportingMetricEntries += + parseTextProto( + textFormatReportingMetricEntry.reader(), + Report.ReportingMetricEntry.getDefaultInstance() + ) } // Either timeIntervals or periodicTimeIntervalInput is set. diff --git a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt index 753763e6bb5..87b7e03399e 100644 --- a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt +++ b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt @@ -66,25 +66,20 @@ import org.wfanet.measurement.reporting.v2alpha.timeIntervals @RunWith(JUnit4::class) class ReportingTest { - private val reportingSetsServiceMock: ReportingSetsCoroutineImplBase = - mockService { - onBlocking { createReportingSet(any()) }.thenReturn(REPORTING_SET) - onBlocking { listReportingSets(any()) }.thenReturn(listReportingSetsResponse { - reportingSets += REPORTING_SET - }) - } - private val reportsServiceMock: ReportsCoroutineImplBase = - mockService { - onBlocking { createReport(any()) }.thenReturn(REPORT) - onBlocking { listReports(any()) }.thenReturn(listReportsResponse { - reports += REPORT - }) - onBlocking { getReport(any()) }.thenReturn(REPORT) - } - private val eventGroupsServiceMock: EventGroupsCoroutineImplBase = - mockService { onBlocking { listEventGroups(any()) }.thenReturn(listEventGroupsResponse { - eventGroups += EVENT_GROUP - }) } + private val reportingSetsServiceMock: ReportingSetsCoroutineImplBase = mockService { + onBlocking { createReportingSet(any()) }.thenReturn(REPORTING_SET) + onBlocking { listReportingSets(any()) } + .thenReturn(listReportingSetsResponse { reportingSets += REPORTING_SET }) + } + private val reportsServiceMock: ReportsCoroutineImplBase = mockService { + onBlocking { createReport(any()) }.thenReturn(REPORT) + onBlocking { listReports(any()) }.thenReturn(listReportsResponse { reports += REPORT }) + onBlocking { getReport(any()) }.thenReturn(REPORT) + } + private val eventGroupsServiceMock: EventGroupsCoroutineImplBase = mockService { + onBlocking { listEventGroups(any()) } + .thenReturn(listEventGroupsResponse { eventGroups += EVENT_GROUP }) + } private val serverCerts = SigningCerts.fromPemFiles( @@ -153,10 +148,11 @@ class ReportingTest { reportingSet = reportingSet { filter = "person.age_group == 1" displayName = "reporting-set" - primitive = ReportingSetKt.primitive { - cmmsEventGroups += CMMS_EVENT_GROUP_NAME_1 - cmmsEventGroups += CMMS_EVENT_GROUP_NAME_2 - } + primitive = + ReportingSetKt.primitive { + cmmsEventGroups += CMMS_EVENT_GROUP_NAME_1 + cmmsEventGroups += CMMS_EVENT_GROUP_NAME_2 + } } reportingSetId = REPORTING_SET_ID } @@ -174,7 +170,8 @@ class ReportingTest { lhs { reporting_set: "$REPORTING_SET_NAME" } - """.trimIndent() + """ + .trimIndent() val args = arrayOf( @@ -194,23 +191,24 @@ class ReportingTest { val output = callCli(args) verifyProtoArgument( - reportingSetsServiceMock, - ReportingSetsCoroutineImplBase::createReportingSet - ) + reportingSetsServiceMock, + ReportingSetsCoroutineImplBase::createReportingSet + ) .isEqualTo( createReportingSetRequest { parent = MEASUREMENT_CONSUMER_NAME reportingSet = reportingSet { filter = "person.age_group == 1" displayName = "reporting-set" - composite = ReportingSetKt.composite { - expression = ReportingSetKt.setExpression { - operation = ReportingSet.SetExpression.Operation.UNION - lhs = ReportingSetKt.SetExpressionKt.operand { - reportingSet = REPORTING_SET_NAME - } + composite = + ReportingSetKt.composite { + expression = + ReportingSetKt.setExpression { + operation = ReportingSet.SetExpression.Operation.UNION + lhs = + ReportingSetKt.SetExpressionKt.operand { reportingSet = REPORTING_SET_NAME } + } } - } } reportingSetId = REPORTING_SET_ID } @@ -228,7 +226,8 @@ class ReportingTest { lhs { reporting_set: "$REPORTING_SET_NAME" } - """.trimIndent() + """ + .trimIndent() val args = arrayOf( @@ -276,14 +275,13 @@ class ReportingTest { } ) assertThat(parseTextProto(output.reader(), ListReportingSetsResponse.getDefaultInstance())) - .isEqualTo(listReportingSetsResponse { - reportingSets += REPORTING_SET - }) + .isEqualTo(listReportingSetsResponse { reportingSets += REPORTING_SET }) } @Test fun `create report with timeIntervalInput calls api with valid request`() { - val textFormatReportingMetricEntryFile = TEXTPROTO_DIR.resolve("reporting_metric_entry.textproto").toFile() + val textFormatReportingMetricEntryFile = + TEXTPROTO_DIR.resolve("reporting_metric_entry.textproto").toFile() val startTime = "2017-01-15T01:30:15.01Z" val endTime = "2018-01-15T01:30:15.01Z" val startTime2 = "2019-01-15T01:30:15.01Z" @@ -316,7 +314,11 @@ class ReportingTest { reportId = REPORT_ID requestId = REPORT_REQUEST_ID report = report { - reportingMetricEntries += parseTextProto(textFormatReportingMetricEntryFile, Report.ReportingMetricEntry.getDefaultInstance()) + reportingMetricEntries += + parseTextProto( + textFormatReportingMetricEntryFile, + Report.ReportingMetricEntry.getDefaultInstance() + ) timeIntervals = timeIntervals { timeIntervals += interval { this.startTime = Instant.parse(startTime).toProtoTime() @@ -336,7 +338,8 @@ class ReportingTest { @Test fun `create report with periodicTimeIntervalInput calls api with valid request`() { - val textFormatReportingMetricEntryFile = TEXTPROTO_DIR.resolve("reporting_metric_entry.textproto").toFile() + val textFormatReportingMetricEntryFile = + TEXTPROTO_DIR.resolve("reporting_metric_entry.textproto").toFile() val startTime = "2017-01-15T01:30:15.01Z" val increment = "P1DT3H5M12.99S" @@ -366,7 +369,11 @@ class ReportingTest { reportId = REPORT_ID requestId = REPORT_REQUEST_ID report = report { - reportingMetricEntries += parseTextProto(textFormatReportingMetricEntryFile, Report.ReportingMetricEntry.getDefaultInstance()) + reportingMetricEntries += + parseTextProto( + textFormatReportingMetricEntryFile, + Report.ReportingMetricEntry.getDefaultInstance() + ) periodicTimeInterval = periodicTimeInterval { this.startTime = Instant.parse(startTime).toProtoTime() this.increment = Duration.parse(increment).toProtoDuration() @@ -381,7 +388,8 @@ class ReportingTest { @Test fun `create report with both periodicTimeIntervalInput and timeIntervalInput fails`() { - val textFormatReportingMetricEntryFile = TEXTPROTO_DIR.resolve("reporting_metric_entry.textproto").toFile() + val textFormatReportingMetricEntryFile = + TEXTPROTO_DIR.resolve("reporting_metric_entry.textproto").toFile() val increment = "P1DT3H5M12.99S" val startTime = "2017-01-15T01:30:15.01Z" val endTime = "2018-01-15T01:30:15.01Z" @@ -478,9 +486,7 @@ class ReportingTest { } ) assertThat(parseTextProto(output.reader(), ListEventGroupsResponse.getDefaultInstance())) - .isEqualTo(listEventGroupsResponse { - eventGroups += EVENT_GROUP - }) + .isEqualTo(listEventGroupsResponse { eventGroups += EVENT_GROUP }) } companion object { @@ -516,20 +522,14 @@ class ReportingTest { private const val REPORTING_SET_ID = "abc" private const val REPORTING_SET_NAME = "reportingSet/$REPORTING_SET_ID" - private val REPORTING_SET = reportingSet { - name = REPORTING_SET_NAME - } + private val REPORTING_SET = reportingSet { name = REPORTING_SET_NAME } private const val REPORT_REQUEST_ID = "def" private const val REPORT_ID = "abc" private const val REPORT_NAME = "$MEASUREMENT_CONSUMER_NAME/reports/$REPORT_ID" - private val REPORT = report { - name = REPORT_NAME - } + private val REPORT = report { name = REPORT_NAME } private const val EVENT_GROUP_NAME = "$MEASUREMENT_CONSUMER_NAME/eventGroups/1" - private val EVENT_GROUP = eventGroup { - name = EVENT_GROUP_NAME - } + private val EVENT_GROUP = eventGroup { name = EVENT_GROUP_NAME } } } From ffc1e04460ee54ea505028bc06daca57c86e0fdd Mon Sep 17 00:00:00 2001 From: Tristan Vuong Date: Fri, 21 Jul 2023 15:34:03 +0000 Subject: [PATCH 3/5] Fix required option. --- .../reporting/service/api/v2alpha/tools/Reporting.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt index bff1fc02647..2e995684e50 100644 --- a/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt +++ b/src/main/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/Reporting.kt @@ -232,7 +232,7 @@ class CreateReportCommand : Runnable { @CommandLine.Option( names = ["--reporting-metric-entry"], description = ["ReportingMetricEntry protobuf messages in text format"], - required = false, + required = true, ) lateinit var textFormatReportingMetricEntries: List From b4d7ab7cec7e2d9769402284b0d6da9fc85a16fc Mon Sep 17 00:00:00 2001 From: Tristan Vuong Date: Fri, 21 Jul 2023 20:30:33 +0000 Subject: [PATCH 4/5] Add additional unit tests --- .../api/v2alpha/tools/ReportingTest.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt index 87b7e03399e..6c00229fcf4 100644 --- a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt +++ b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/ReportingTest.kt @@ -249,6 +249,25 @@ class ReportingTest { CommandLineTesting.assertExitsWith(2) { Reporting.main(args) } } + @Test + fun `create reporting set with neither --set-expression nor --cmms-event-groups fails`() { + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reporting-sets", + "create", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--filter=person.age_group == 1", + "--display-name=reporting-set", + "--id=$REPORTING_SET_ID", + ) + + CommandLineTesting.assertExitsWith(2) { Reporting.main(args) } + } + @Test fun `list reporting sets calls api with valid request`() { val args = @@ -420,6 +439,30 @@ class ReportingTest { CommandLineTesting.assertExitsWith(2) { Reporting.main(args) } } + @Test + fun `create report with no --reporting-metric-entry fails`() { + val increment = "P1DT3H5M12.99S" + val startTime = "2017-01-15T01:30:15.01Z" + + val args = + arrayOf( + "--tls-cert-file=$SECRETS_DIR/mc_tls.pem", + "--tls-key-file=$SECRETS_DIR/mc_tls.key", + "--cert-collection-file=$SECRETS_DIR/reporting_root.pem", + "--reporting-server-api-target=$HOST:${server.port}", + "reports", + "create", + "--parent=$MEASUREMENT_CONSUMER_NAME", + "--periodic-interval-start-time=$startTime", + "--periodic-interval-increment=$increment", + "--periodic-interval-count=3", + "--id=$REPORT_ID", + "--request-id=$REPORT_REQUEST_ID", + ) + + CommandLineTesting.assertExitsWith(2) { Reporting.main(args) } + } + @Test fun `list reports calls api with valid request`() { val args = From 5bdd06c3100544a9deee57789e401d9e9a6de9ed Mon Sep 17 00:00:00 2001 From: Tristan Vuong Date: Tue, 25 Jul 2023 14:55:25 +0000 Subject: [PATCH 5/5] Add missing new line --- .../service/api/v2alpha/tools/reporting_metric_entry.textproto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/reporting_metric_entry.textproto b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/reporting_metric_entry.textproto index 3d57c279386..a6926540ad7 100644 --- a/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/reporting_metric_entry.textproto +++ b/src/test/kotlin/org/wfanet/measurement/reporting/service/api/v2alpha/tools/reporting_metric_entry.textproto @@ -16,4 +16,4 @@ value { } } } -} \ No newline at end of file +}