Revert "JSON source generator includes error estimates"
This reverts commit c8acc57e82c7626823943f47230f0a6fb2f3769e.
Reason for revert: Unable to copy into google3
Original change's description:
> JSON source generator includes error estimates
>
> Initially, an error estimate is included for each privacy level using a
> default population of 100K.
>
> Change-Id: Ibfb7b91c90880863c73e3ff55a9713ed5a2ef458
> Reviewed-on: https://fuchsia-review.googlesource.com/c/cobalt/+/460025
> Reviewed-by: Alexandre Zani <azani@google.com>
> Commit-Queue: Jared Weinstein <jaredweinstein@google.com>
TBR=azani@google.com,jaredweinstein@google.com
# Not skipping CQ checks because original CL landed > 1 day ago.
Change-Id: I0fe590a607f057a30a5bf227a955d857b5fd8d6a
Reviewed-on: https://fuchsia-review.googlesource.com/c/cobalt/+/466196
Reviewed-by: Jared Weinstein <jaredweinstein@google.com>
Reviewed-by: Cameron Dale <camrdale@google.com>
Commit-Queue: Jared Weinstein <jaredweinstein@google.com>
diff --git a/src/bin/config_parser/src/source_generator/json.go b/src/bin/config_parser/src/source_generator/json.go
index 36c4bb3..3f974c6 100644
--- a/src/bin/config_parser/src/source_generator/json.go
+++ b/src/bin/config_parser/src/source_generator/json.go
@@ -8,14 +8,8 @@
import (
"config"
"encoding/json"
- "fmt"
- "privacy"
)
-type jsonOutputter struct {
- privacyParamsPath string
-}
-
// JSON export structure
// go tags set the field name when exported to JSON data.
type jsonRegistry struct {
@@ -54,71 +48,25 @@
Reports []jsonReport `json:"reports"`
}
-type errorDict map[string]jsonErrorEstimate
-
type jsonReport struct {
- Name string `json:"name"`
- Id uint32 `json:"id"`
- ReportType string `json:"report_type"`
- ReportTypeId int32 `json:"report_type_id"`
- LocalPrivacyNoiseLevel string `json:"local_privacy_noise_level"`
- PrivacyLevel string `json:"privacy_level"`
- ErrorEstimates errorDict `json:"error_estimates"`
- CandidateFile string `json:"candidate_file"`
- CandidateList []string `json:"candidate_list"`
- SystemProfileField []string `json:"system_profile_field"`
- AggregationType string `json:"aggregation_type"`
- WindowSize []string `json:"window_size"`
- LocalAggregationPeriod int32 `json:"local_aggregation_period"`
- LocalAggregationProcedure string `json:"local_aggregation_procedure"`
- LocalAggregationProcedurePercentileN uint32 `json:"local_aggregation_procedure_percentile_n"`
-}
-
-type jsonErrorEstimate struct {
- Population uint32 `json:"population"`
- Epsilon float64 `json:"epsilon"`
- Estimate float64 `json:"estimate"`
-}
-
-func (jo *jsonOutputter) makeErrorEstimates(report *config.ReportDefinition, metric *config.MetricDefinition) (estimate errorDict, err error) {
- paramsCalc, err := privacy.NewPrivacyEncodingParamsCalculator(jo.privacyParamsPath)
- if err != nil {
- return nil, err
- }
-
- errorCalc := privacy.NewErrorCalculator(*paramsCalc)
- if err != nil {
- return nil, err
- }
-
- population := report.GetReportingThreshold()
- if population == 0 {
- population = 100000
- }
-
- var estimates = errorDict{}
- for l, _ := range config.ReportDefinition_PrivacyLevel_name {
- level := config.ReportDefinition_PrivacyLevel(l)
- epsilon := paramsCalc.Constants.EpsilonForPrivacyLevel[level]
- errorValue, err := errorCalc.Estimate(metric, report, epsilon, uint64(population))
- if err == nil {
- estimates[level.String()] = jsonErrorEstimate{
- Population: population,
- Epsilon: epsilon,
- Estimate: errorValue,
- }
- }
- }
- if len(estimates) == 0 {
- // Most likely, error estimation is not supported for this specific
- // report/metric configuration.
- return nil, nil
- }
- return estimates, nil
+ Name string `json:"name"`
+ Id uint32 `json:"id"`
+ ReportType string `json:"report_type"`
+ ReportTypeId int32 `json:"report_type_id"`
+ LocalPrivacyNoiseLevel string `json:"local_privacy_noise_level"`
+ PrivacyLevel string `json:"privacy_level"`
+ CandidateFile string `json:"candidate_file"`
+ CandidateList []string `json:"candidate_list"`
+ SystemProfileField []string `json:"system_profile_field"`
+ AggregationType string `json:"aggregation_type"`
+ WindowSize []string `json:"window_size"`
+ LocalAggregationPeriod int32 `json:"local_aggregation_period"`
+ LocalAggregationProcedure string `json:"local_aggregation_procedure"`
+ LocalAggregationProcedurePercentileN uint32 `json:"local_aggregation_procedure_percentile_n"`
}
// JSON struct constructors
-func (jo *jsonOutputter) makeJSONReport(report *config.ReportDefinition, metric *config.MetricDefinition) jsonReport {
+func makeJSONReport(report *config.ReportDefinition) jsonReport {
if report == nil {
return jsonReport{}
}
@@ -131,11 +79,7 @@
var windowSize []string
for _, ws := range report.GetWindowSize() {
windowSize = append(windowSize, ws.String())
- }
- estimate, err := jo.makeErrorEstimates(report, metric)
- if err != nil {
- fmt.Println(err)
}
return jsonReport{
@@ -145,7 +89,6 @@
ReportTypeId: int32(report.GetReportType()),
LocalPrivacyNoiseLevel: report.GetLocalPrivacyNoiseLevel().String(),
PrivacyLevel: report.GetPrivacyLevel().String(),
- ErrorEstimates: estimate,
CandidateFile: report.GetCandidateFile(),
CandidateList: report.GetCandidateList(),
SystemProfileField: systemProfileField,
@@ -157,7 +100,7 @@
}
}
-func (jo *jsonOutputter) makeJSONMetric(metric *config.MetricDefinition) jsonMetric {
+func makeJSONMetric(metric *config.MetricDefinition) jsonMetric {
if metric == nil {
return jsonMetric{}
}
@@ -170,7 +113,7 @@
}
var reports []jsonReport
for _, r := range metric.GetReports() {
- reports = append(reports, jo.makeJSONReport(r, metric))
+ reports = append(reports, makeJSONReport(r))
}
var dimensions []string
@@ -205,14 +148,14 @@
}
}
-func (jo *jsonOutputter) makeJSONProject(project *config.ProjectConfig) jsonProject {
+func makeJSONProject(project *config.ProjectConfig) jsonProject {
if project == nil {
return jsonProject{}
}
var metrics []jsonMetric
for _, m := range project.GetMetrics() {
- metrics = append(metrics, jo.makeJSONMetric(m))
+ metrics = append(metrics, makeJSONMetric(m))
}
return jsonProject{
@@ -223,14 +166,14 @@
}
}
-func (jo *jsonOutputter) makeJSONCustomer(customer *config.CustomerConfig) jsonCustomer {
+func makeJSONCustomer(customer *config.CustomerConfig) jsonCustomer {
if customer == nil {
return jsonCustomer{}
}
var projects []jsonProject
for _, p := range customer.GetProjects() {
- projects = append(projects, jo.makeJSONProject(p))
+ projects = append(projects, makeJSONProject(p))
}
return jsonCustomer{
@@ -240,14 +183,14 @@
}
}
-func (jo *jsonOutputter) makeJSONRegistry(registry *config.CobaltRegistry) jsonRegistry {
+func makeJSONRegistry(registry *config.CobaltRegistry) jsonRegistry {
if registry == nil {
return jsonRegistry{}
}
var customers []jsonCustomer
for _, c := range registry.GetCustomers() {
- customers = append(customers, jo.makeJSONCustomer(c))
+ customers = append(customers, makeJSONCustomer(c))
}
return jsonRegistry{
@@ -256,8 +199,8 @@
}
// Outputs the registry contents in JSON
-func (jo *jsonOutputter) JSONOutput(_, filtered *config.CobaltRegistry) (outputBytes []byte, err error) {
- jsonRegistry := jo.makeJSONRegistry(filtered)
+func JSONOutput(_, filtered *config.CobaltRegistry) (outputBytes []byte, err error) {
+ jsonRegistry := makeJSONRegistry(filtered)
prefix := ""
indent := " "
@@ -268,19 +211,3 @@
return jsonBytes, nil
}
-
-// TODO: (jaredweinstein) Remove this deprecated method once call sites are switched.
-func JSONOutput(_, filtered *config.CobaltRegistry) (outputBytes []byte, err error) {
- // Invalid privacy param path only prevents error estimate struct from being
- // properly populated.
- jo := jsonOutputter{""}
- return jo.JSONOutput(nil, filtered)
-}
-
-// Returns an output formatter for JSON
-//
-// privacyParamsPath is the string path of the privacy params file to be used for error estimation.
-func JSONOutputFactory(privacyParamsPath string) OutputFormatter {
- jo := jsonOutputter{privacyParamsPath}
- return jo.JSONOutput
-}
diff --git a/src/bin/config_parser/src/source_generator/json_test.go b/src/bin/config_parser/src/source_generator/json_test.go
index 40b71c0..b700ea5 100644
--- a/src/bin/config_parser/src/source_generator/json_test.go
+++ b/src/bin/config_parser/src/source_generator/json_test.go
@@ -12,68 +12,29 @@
"testing"
)
-var jo = jsonOutputter{privacyParamsTestPath()}
-
func TestConstructorsHandleNil(t *testing.T) {
- r := jo.makeJSONReport(nil, nil)
+ r := makeJSONReport(nil)
if reflect.DeepEqual(r, (jsonReport{})) == false {
t.Errorf("makeJSONReport failed to return empty report got = %#v", r)
}
- m := jo.makeJSONMetric(nil)
+ m := makeJSONMetric(nil)
if reflect.DeepEqual(m, (jsonMetric{})) == false {
t.Errorf("makeJSONMetric failed to return empty metric got = %#v", m)
}
- p := jo.makeJSONProject(nil)
+ p := makeJSONProject(nil)
if reflect.DeepEqual(p, (jsonProject{})) == false {
t.Errorf("makeJSONProject failed to return empty project got = %#v", p)
}
- c := jo.makeJSONCustomer(nil)
+ c := makeJSONCustomer(nil)
if reflect.DeepEqual(c, (jsonCustomer{})) == false {
t.Errorf("makeJSONCustomer failed to return empty customer got = %#v", c)
}
- rg := jo.makeJSONRegistry(nil)
+ rg := makeJSONRegistry(nil)
if reflect.DeepEqual(rg, (jsonRegistry{})) == false {
t.Errorf("makeJSONRegistry failed to return empty registry got = %#v", rg)
}
}
-func TestMakeErrorEstimates(t *testing.T) {
- r := config.ReportDefinition{
- ReportType: config.ReportDefinition_UNIQUE_DEVICE_COUNTS,
- PrivacyLevel: config.ReportDefinition_LOW_PRIVACY,
- LocalAggregationProcedure: config.ReportDefinition_SELECT_FIRST,
- }
- m := config.MetricDefinition{
- MetricType: config.MetricDefinition_OCCURRENCE,
- }
-
- want := errorDict{
- "LOW_PRIVACY": jsonErrorEstimate{
- Population: 100000,
- Epsilon: 10,
- Estimate: 4.195226071845181,
- },
- "MEDIUM_PRIVACY": jsonErrorEstimate{
- Population: 100000,
- Epsilon: 5,
- Estimate: 9.532416856027137,
- },
- "HIGH_PRIVACY": jsonErrorEstimate{
- Population: 100000,
- Epsilon: 1,
- Estimate: 15.607457540599944,
- },
- }
-
- got, err := jo.makeErrorEstimates(&r, &m)
- if err != nil {
- }
-
- if reflect.DeepEqual(want, got) == false {
- t.Errorf("makeJSONReport(%#v)\n\n GOT: %#v\nWANT: %#v", r, got, want)
- }
-}
-
func TestMakeJSONReport(t *testing.T) {
name := "test_name"
id := uint32(123456789)
@@ -97,11 +58,6 @@
LocalAggregationProcedure: config.ReportDefinition_SUM_PROCEDURE,
LocalAggregationProcedurePercentileN: aggregation_percentile,
}
- m := config.MetricDefinition{
- MetricName: name,
- Id: id,
- MetricType: config.MetricDefinition_EVENT_OCCURRED,
- }
want := jsonReport{
Name: name,
@@ -110,7 +66,6 @@
ReportTypeId: int32(config.ReportDefinition_SIMPLE_OCCURRENCE_COUNT),
LocalPrivacyNoiseLevel: "NOISE_LEVEL_UNSET",
PrivacyLevel: "LOW_PRIVACY",
- ErrorEstimates: nil,
CandidateFile: candidate_file,
CandidateList: candidate_list,
SystemProfileField: []string{"OS", "ARCH"},
@@ -121,7 +76,7 @@
LocalAggregationProcedurePercentileN: aggregation_percentile,
}
- got := jo.makeJSONReport(&r, &m)
+ got := makeJSONReport(&r)
if reflect.DeepEqual(want, got) == false {
t.Errorf("makeJSONReport(%#v)\n\n GOT: %#v\nWANT: %#v", r, got, want)
}
@@ -190,7 +145,7 @@
Reports: emptyReports,
}
- got := jo.makeJSONMetric(&m)
+ got := makeJSONMetric(&m)
if reflect.DeepEqual(want, got) == false {
t.Errorf("makeJSONMetric(%#v)\n\n GOT: %#v\nWANT: %#v", m, got, want)
}
@@ -213,7 +168,7 @@
Reports: emptyReports,
}
- got := jo.makeJSONMetric(&m)
+ got := makeJSONMetric(&m)
if reflect.DeepEqual(want, got) == false {
t.Errorf("makeJSONMetric(%#v)\n\n GOT: %#v\nWANT: %#v", m, got, want)
}
@@ -239,7 +194,7 @@
Metrics: emptyMetrics,
}
- got := jo.makeJSONProject(&p)
+ got := makeJSONProject(&p)
if reflect.DeepEqual(want, got) == false {
t.Errorf("makeJSONProject(%#v)\n\n GOT: %#v\nWANT: %#v", p, got, want)
}
@@ -262,7 +217,7 @@
Projects: emptyProjects,
}
- got := jo.makeJSONCustomer(&c)
+ got := makeJSONCustomer(&c)
if reflect.DeepEqual(want, got) == false {
t.Errorf("makeJSONCustomer(%#v)\n\n GOT: %#v\nWANT: %#v", c, got, want)
}
@@ -274,12 +229,12 @@
Customers: customers,
}
- emptyCustomers := []jsonCustomer{jsonCustomer{}, jsonCustomer{}}
+ emptyProjects := []jsonCustomer{jsonCustomer{}, jsonCustomer{}}
want := jsonRegistry{
- Customers: emptyCustomers,
+ Customers: emptyProjects,
}
- got := jo.makeJSONRegistry(&r)
+ got := makeJSONRegistry(&r)
if reflect.DeepEqual(want, got) == false {
t.Errorf("makeJSONRegistry(%#v)\n\n GOT: %#v\nWANT: %#v", r, got, want)
}
diff --git a/src/bin/config_parser/src/source_generator/source_generator_test.go b/src/bin/config_parser/src/source_generator/source_generator_test.go
index 1a2abe0..614f62f 100644
--- a/src/bin/config_parser/src/source_generator/source_generator_test.go
+++ b/src/bin/config_parser/src/source_generator/source_generator_test.go
@@ -22,13 +22,6 @@
"github.com/google/go-cmp/cmp"
)
-func privacyParamsTestPath() string {
- _, fullPath, _, _ := runtime.Caller(1)
- cobaltRoot := strings.Split(fullPath, "out/gen")[0]
- privacyParamsPath := path.Join(cobaltRoot, "src", "algorithms", "privacy", "data", "privacy_encoding_params")
- return privacyParamsPath
-}
-
type memConfigReader struct {
customers string
projects map[string]string
@@ -204,7 +197,7 @@
{"GoldenForTesting.java", JavaOutputFactory("config", []string{}, []string{"testing"}, "golden_for_testing"), false},
// JSONOutput() ignores hideOnClients so we only test once
- {"golden.cb.json", JSONOutputFactory(privacyParamsTestPath()), true},
+ {"golden.cb.json", JSONOutput, true},
}
func TestPrintConfig(t *testing.T) {
diff --git a/src/bin/config_parser/src/source_generator/source_generator_test_files/golden.cb.json b/src/bin/config_parser/src/source_generator/source_generator_test_files/golden.cb.json
index 3e54111..0cf15d6 100644
--- a/src/bin/config_parser/src/source_generator/source_generator_test_files/golden.cb.json
+++ b/src/bin/config_parser/src/source_generator/source_generator_test_files/golden.cb.json
@@ -33,7 +33,6 @@
"report_type_id": 9999,
"local_privacy_noise_level": "NOISE_LEVEL_UNSET",
"privacy_level": "PRIVACY_LEVEL_UNKNOWN",
- "error_estimates": null,
"candidate_file": "",
"candidate_list": null,
"system_profile_field": null,
@@ -50,7 +49,6 @@
"report_type_id": 3,
"local_privacy_noise_level": "NOISE_LEVEL_UNSET",
"privacy_level": "PRIVACY_LEVEL_UNKNOWN",
- "error_estimates": null,
"candidate_file": "",
"candidate_list": null,
"system_profile_field": null,
@@ -90,7 +88,6 @@
"report_type_id": 1,
"local_privacy_noise_level": "NOISE_LEVEL_UNSET",
"privacy_level": "PRIVACY_LEVEL_UNKNOWN",
- "error_estimates": null,
"candidate_file": "",
"candidate_list": null,
"system_profile_field": null,
@@ -134,7 +131,6 @@
"report_type_id": 1,
"local_privacy_noise_level": "NOISE_LEVEL_UNSET",
"privacy_level": "LOW_PRIVACY",
- "error_estimates": null,
"candidate_file": "",
"candidate_list": null,
"system_profile_field": null,
@@ -188,7 +184,6 @@
"report_type_id": 0,
"local_privacy_noise_level": "NOISE_LEVEL_UNSET",
"privacy_level": "PRIVACY_LEVEL_UNKNOWN",
- "error_estimates": null,
"candidate_file": "",
"candidate_list": null,
"system_profile_field": null,
diff --git a/src/bin/config_parser/src/source_generator/source_outputter.go b/src/bin/config_parser/src/source_generator/source_outputter.go
index c6afa04..2ab9b8f 100644
--- a/src/bin/config_parser/src/source_generator/source_outputter.go
+++ b/src/bin/config_parser/src/source_generator/source_outputter.go
@@ -442,7 +442,7 @@
}
}
-func getOutputFormatter(format, namespace, goPackageName, varName string, features []string, outFilename string, privacyParamsPath string) (OutputFormatter, error) {
+func getOutputFormatter(format, namespace, goPackageName, varName string, features []string, outFilename string) (OutputFormatter, error) {
namespaceList := []string{}
if namespace != "" {
namespaceList = strings.Split(namespace, ".")
@@ -464,7 +464,7 @@
case "java":
return JavaOutputFactory(varName, namespaceList, features, outFilename), nil
case "json":
- return JSONOutputFactory(privacyParamsPath), nil
+ return JSONOutput, nil
case "rust":
return RustOutputFactory(varName, namespaceList, features), nil
default:
diff --git a/src/bin/config_parser/src/source_generator/source_outputter_test.go b/src/bin/config_parser/src/source_generator/source_outputter_test.go
index fb6df4c..f8df45d 100644
--- a/src/bin/config_parser/src/source_generator/source_outputter_test.go
+++ b/src/bin/config_parser/src/source_generator/source_outputter_test.go
@@ -10,7 +10,7 @@
formats := []string{"bin", "b64", "cpp", "dart", "rust", "go", "java", "json"}
for _, format := range formats {
- outputFormatter, err := getOutputFormatter(format, "ns", "package", "varName", []string{}, "", "privacy/path")
+ outputFormatter, err := getOutputFormatter(format, "ns", "package", "varName", []string{}, "")
if outputFormatter == nil {
t.Errorf("Unexpected nil output formatter for format %v", format)
}
@@ -19,7 +19,7 @@
}
}
- outputFormatter, err := getOutputFormatter("blah", "ns", "package", "varName", []string{}, "", "privacy/path")
+ outputFormatter, err := getOutputFormatter("blah", "ns", "package", "varName", []string{}, "")
if outputFormatter != nil {
t.Errorf("Unexpectedly got an output formatter.")
}
diff --git a/src/bin/config_parser/src/source_generator/writer.go b/src/bin/config_parser/src/source_generator/writer.go
index 655c489..e485458 100644
--- a/src/bin/config_parser/src/source_generator/writer.go
+++ b/src/bin/config_parser/src/source_generator/writer.go
@@ -15,19 +15,18 @@
)
var (
- addFileSuffix = flag.Bool("add_file_suffix", false, "Append the out_format to the out_file, even if there is only one out_format specified")
- outFile = flag.String("output_file", "", "File to which the serialized config should be written. Defaults to stdout. When multiple output formats are specified, it will append the format to the filename")
- outFilename = flag.String("out_filename", "", "The base name to use for writing files. Should not be used with output_file.")
- outDir = flag.String("out_dir", "", "The directory into which files should be written.")
- outFormat = flag.String("out_format", "bin", "Specifies the output formats (separated by ' '). Supports 'bin' (serialized proto), 'go' (a golang package), 'b64' (serialized proto to base 64), 'cpp' (a C++ file containing a variable with a base64-encoded serialized proto) 'dart' (a Dart library), 'json' (a JSON object), and 'rust' (a rust crate)")
- features = flag.String("features", "", "A comma separated list of source generator features to enable.")
- namespace = flag.String("namespace", "", "When using the 'cpp', 'rust', or 'go' output format, this will specify the period-separated namespace within which the config variable must be placed (this will be transformed into an underscore-separated package name for go).")
- goPackageName = flag.String("go_package", "", "When using the 'go' output format, this will specify the package for generated code.")
- dartOutDir = flag.String("dart_out_dir", "", "The directory to write dart files to (if different from out_dir)")
- varName = flag.String("var_name", "config", "When using the 'cpp' or 'dart' output format, this will specify the variable name to be used in the output.")
- privacyParamsPath = flag.String("privacy_params_path", "", "For output formats that require an error estimate, this specifies the path to a file containing required privacy parameters.")
- checkOnly = flag.Bool("check_only", false, "Only check that the configuration is valid.")
- allowEmptyOutput = flag.Bool("allow_empty_output", false, "Relax the requirement that the cobalt registry output not be empty.")
+ addFileSuffix = flag.Bool("add_file_suffix", false, "Append the out_format to the out_file, even if there is only one out_format specified")
+ outFile = flag.String("output_file", "", "File to which the serialized config should be written. Defaults to stdout. When multiple output formats are specified, it will append the format to the filename")
+ outFilename = flag.String("out_filename", "", "The base name to use for writing files. Should not be used with output_file.")
+ outDir = flag.String("out_dir", "", "The directory into which files should be written.")
+ outFormat = flag.String("out_format", "bin", "Specifies the output formats (separated by ' '). Supports 'bin' (serialized proto), 'go' (a golang package), 'b64' (serialized proto to base 64), 'cpp' (a C++ file containing a variable with a base64-encoded serialized proto) 'dart' (a Dart library), 'json' (a JSON object), and 'rust' (a rust crate)")
+ features = flag.String("features", "", "A comma separated list of source generator features to enable.")
+ namespace = flag.String("namespace", "", "When using the 'cpp', 'rust', or 'go' output format, this will specify the period-separated namespace within which the config variable must be placed (this will be transformed into an underscore-separated package name for go).")
+ goPackageName = flag.String("go_package", "", "When using the 'go' output format, this will specify the package for generated code.")
+ dartOutDir = flag.String("dart_out_dir", "", "The directory to write dart files to (if different from out_dir)")
+ varName = flag.String("var_name", "config", "When using the 'cpp' or 'dart' output format, this will specify the variable name to be used in the output.")
+ checkOnly = flag.Bool("check_only", false, "Only check that the configuration is valid.")
+ allowEmptyOutput = flag.Bool("allow_empty_output", false, "Relax the requirement that the cobalt registry output not be empty.")
)
// checkFlags verifies that the specified flags are compatible with each other.
@@ -87,7 +86,7 @@
generateFilename := filenameGeneratorFromFlags()
features := strings.Split(*features, ",")
for _, format := range parseOutFormatList(*outFormat) {
- outputFormatter, err := getOutputFormatter(format, *namespace, *goPackageName, *varName, features, *outFilename, *privacyParamsPath)
+ outputFormatter, err := getOutputFormatter(format, *namespace, *goPackageName, *varName, features, *outFilename)
if err != nil {
return err
}