File: //opt/go/pkg/mod/github.com/prometheus/
[email protected]/expfmt/openmetrics_create_test.go
// Copyright 2020 The Prometheus 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 expfmt
import (
"bytes"
"math"
"strings"
"testing"
"time"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
"github.com/prometheus/common/model"
)
func TestCreateOpenMetrics(t *testing.T) {
openMetricsTimestamp := timestamppb.New(time.Unix(12345, 600000000))
if err := openMetricsTimestamp.CheckValid(); err != nil {
t.Error(err)
}
oldDefaultScheme := model.NameEscapingScheme
model.NameEscapingScheme = model.NoEscaping
defer func() {
model.NameEscapingScheme = oldDefaultScheme
}()
scenarios := []struct {
in *dto.MetricFamily
options []EncoderOption
out string
}{
// 0: Counter, timestamp given, no _total suffix.
{
in: &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("two-line\n doc str\\ing"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("labelname"),
Value: proto.String("val1"),
},
{
Name: proto.String("basename"),
Value: proto.String("basevalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(42),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("labelname"),
Value: proto.String("val2"),
},
{
Name: proto.String("basename"),
Value: proto.String("basevalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(.23),
},
TimestampMs: proto.Int64(1234567890),
},
},
},
out: `# HELP name two-line\n doc str\\ing
# TYPE name unknown
name{labelname="val1",basename="basevalue"} 42.0
name{labelname="val2",basename="basevalue"} 0.23 1.23456789e+06
`,
},
// 1: Dots in name
{
in: &dto.MetricFamily{
Name: proto.String("name.with.dots"),
Help: proto.String("boring help"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("labelname"),
Value: proto.String("val1"),
},
{
Name: proto.String("basename"),
Value: proto.String("basevalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(42),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("labelname"),
Value: proto.String("val2"),
},
{
Name: proto.String("basename"),
Value: proto.String("basevalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(.23),
},
TimestampMs: proto.Int64(1234567890),
},
},
},
out: `# HELP "name.with.dots" boring help
# TYPE "name.with.dots" unknown
{"name.with.dots",labelname="val1",basename="basevalue"} 42.0
{"name.with.dots",labelname="val2",basename="basevalue"} 0.23 1.23456789e+06
`,
},
// 2: Dots in name, no labels
{
in: &dto.MetricFamily{
Name: proto.String("name.with.dots"),
Help: proto.String("boring help"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(42),
},
},
{
Counter: &dto.Counter{
Value: proto.Float64(.23),
},
TimestampMs: proto.Int64(1234567890),
},
},
},
out: `# HELP "name.with.dots" boring help
# TYPE "name.with.dots" unknown
{"name.with.dots"} 42.0
{"name.with.dots"} 0.23 1.23456789e+06
`,
},
// 3: Gauge, some escaping required, +Inf as value, multi-byte characters in label values.
{
in: &dto.MetricFamily{
Name: proto.String("gauge_name"),
Help: proto.String("gauge\ndoc\nstr\"ing"),
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("name_1"),
Value: proto.String("val with\nnew line"),
},
{
Name: proto.String("name_2"),
Value: proto.String("val with \\backslash and \"quotes\""),
},
},
Gauge: &dto.Gauge{
Value: proto.Float64(math.Inf(+1)),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("name_1"),
Value: proto.String("Björn"),
},
{
Name: proto.String("name_2"),
Value: proto.String("佖佥"),
},
},
Gauge: &dto.Gauge{
Value: proto.Float64(3.14e42),
},
},
},
},
out: `# HELP gauge_name gauge\ndoc\nstr\"ing
# TYPE gauge_name gauge
gauge_name{name_1="val with\nnew line",name_2="val with \\backslash and \"quotes\""} +Inf
gauge_name{name_1="Björn",name_2="佖佥"} 3.14e+42
`,
},
// 4: Gauge, utf-8, some escaping required, +Inf as value, multi-byte characters in label values.
{
in: &dto.MetricFamily{
Name: proto.String("gauge.name\""),
Help: proto.String("gauge\ndoc\nstr\"ing"),
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("name.1"),
Value: proto.String("val with\nnew line"),
},
{
Name: proto.String("name*2"),
Value: proto.String("val with \\backslash and \"quotes\""),
},
},
Gauge: &dto.Gauge{
Value: proto.Float64(math.Inf(+1)),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("name.1"),
Value: proto.String("Björn"),
},
{
Name: proto.String("name*2"),
Value: proto.String("佖佥"),
},
},
Gauge: &dto.Gauge{
Value: proto.Float64(3.14e42),
},
},
},
},
out: `# HELP "gauge.name\"" gauge\ndoc\nstr\"ing
# TYPE "gauge.name\"" gauge
{"gauge.name\"","name.1"="val with\nnew line","name*2"="val with \\backslash and \"quotes\""} +Inf
{"gauge.name\"","name.1"="Björn","name*2"="佖佥"} 3.14e+42
`,
},
// 5: Unknown, no help, one sample with no labels and -Inf as value, another sample with one label.
{
in: &dto.MetricFamily{
Name: proto.String("unknown_name"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
{
Untyped: &dto.Untyped{
Value: proto.Float64(math.Inf(-1)),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("name_1"),
Value: proto.String("value 1"),
},
},
Untyped: &dto.Untyped{
Value: proto.Float64(-1.23e-45),
},
},
},
},
out: `# TYPE unknown_name unknown
unknown_name -Inf
unknown_name{name_1="value 1"} -1.23e-45
`,
},
// 6: Summary.
{
in: &dto.MetricFamily{
Name: proto.String("summary_name"),
Help: proto.String("summary docstring"),
Type: dto.MetricType_SUMMARY.Enum(),
Metric: []*dto.Metric{
{
Summary: &dto.Summary{
SampleCount: proto.Uint64(42),
SampleSum: proto.Float64(-3.4567),
Quantile: []*dto.Quantile{
{
Quantile: proto.Float64(0.5),
Value: proto.Float64(-1.23),
},
{
Quantile: proto.Float64(0.9),
Value: proto.Float64(.2342354),
},
{
Quantile: proto.Float64(0.99),
Value: proto.Float64(0),
},
},
CreatedTimestamp: openMetricsTimestamp,
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("name_1"),
Value: proto.String("value 1"),
},
{
Name: proto.String("name_2"),
Value: proto.String("value 2"),
},
},
Summary: &dto.Summary{
SampleCount: proto.Uint64(4711),
SampleSum: proto.Float64(2010.1971),
Quantile: []*dto.Quantile{
{
Quantile: proto.Float64(0.5),
Value: proto.Float64(1),
},
{
Quantile: proto.Float64(0.9),
Value: proto.Float64(2),
},
{
Quantile: proto.Float64(0.99),
Value: proto.Float64(3),
},
},
CreatedTimestamp: openMetricsTimestamp,
},
},
},
},
options: []EncoderOption{WithCreatedLines()},
out: `# HELP summary_name summary docstring
# TYPE summary_name summary
summary_name{quantile="0.5"} -1.23
summary_name{quantile="0.9"} 0.2342354
summary_name{quantile="0.99"} 0.0
summary_name_sum -3.4567
summary_name_count 42
summary_name_created 12345.6
summary_name{name_1="value 1",name_2="value 2",quantile="0.5"} 1.0
summary_name{name_1="value 1",name_2="value 2",quantile="0.9"} 2.0
summary_name{name_1="value 1",name_2="value 2",quantile="0.99"} 3.0
summary_name_sum{name_1="value 1",name_2="value 2"} 2010.1971
summary_name_count{name_1="value 1",name_2="value 2"} 4711
summary_name_created{name_1="value 1",name_2="value 2"} 12345.6
`,
},
// 7: Histogram
{
in: &dto.MetricFamily{
Name: proto.String("request_duration_microseconds"),
Help: proto.String("The response latency."),
Type: dto.MetricType_HISTOGRAM.Enum(),
Unit: proto.String("microseconds"),
Metric: []*dto.Metric{
{
Histogram: &dto.Histogram{
SampleCount: proto.Uint64(2693),
SampleSum: proto.Float64(1756047.3),
Bucket: []*dto.Bucket{
{
UpperBound: proto.Float64(100),
CumulativeCount: proto.Uint64(123),
},
{
UpperBound: proto.Float64(120),
CumulativeCount: proto.Uint64(412),
},
{
UpperBound: proto.Float64(144),
CumulativeCount: proto.Uint64(592),
},
{
UpperBound: proto.Float64(172.8),
CumulativeCount: proto.Uint64(1524),
},
{
UpperBound: proto.Float64(math.Inf(+1)),
CumulativeCount: proto.Uint64(2693),
},
},
CreatedTimestamp: openMetricsTimestamp,
},
},
},
},
options: []EncoderOption{WithCreatedLines(), WithUnit()},
out: `# HELP request_duration_microseconds The response latency.
# TYPE request_duration_microseconds histogram
# UNIT request_duration_microseconds microseconds
request_duration_microseconds_bucket{le="100.0"} 123
request_duration_microseconds_bucket{le="120.0"} 412
request_duration_microseconds_bucket{le="144.0"} 592
request_duration_microseconds_bucket{le="172.8"} 1524
request_duration_microseconds_bucket{le="+Inf"} 2693
request_duration_microseconds_sum 1.7560473e+06
request_duration_microseconds_count 2693
request_duration_microseconds_created 12345.6
`,
},
// 8: Histogram with missing +Inf bucket.
{
in: &dto.MetricFamily{
Name: proto.String("request_duration_microseconds"),
Help: proto.String("The response latency."),
Type: dto.MetricType_HISTOGRAM.Enum(),
Unit: proto.String("microseconds"),
Metric: []*dto.Metric{
{
Histogram: &dto.Histogram{
SampleCount: proto.Uint64(2693),
SampleSum: proto.Float64(1756047.3),
Bucket: []*dto.Bucket{
{
UpperBound: proto.Float64(100),
CumulativeCount: proto.Uint64(123),
},
{
UpperBound: proto.Float64(120),
CumulativeCount: proto.Uint64(412),
},
{
UpperBound: proto.Float64(144),
CumulativeCount: proto.Uint64(592),
},
{
UpperBound: proto.Float64(172.8),
CumulativeCount: proto.Uint64(1524),
},
},
},
},
},
},
out: `# HELP request_duration_microseconds The response latency.
# TYPE request_duration_microseconds histogram
request_duration_microseconds_bucket{le="100.0"} 123
request_duration_microseconds_bucket{le="120.0"} 412
request_duration_microseconds_bucket{le="144.0"} 592
request_duration_microseconds_bucket{le="172.8"} 1524
request_duration_microseconds_bucket{le="+Inf"} 2693
request_duration_microseconds_sum 1.7560473e+06
request_duration_microseconds_count 2693
`,
},
// 9: Histogram with missing +Inf bucket but with different exemplars.
{
in: &dto.MetricFamily{
Name: proto.String("request_duration_microseconds"),
Help: proto.String("The response latency."),
Type: dto.MetricType_HISTOGRAM.Enum(),
Metric: []*dto.Metric{
{
Histogram: &dto.Histogram{
SampleCount: proto.Uint64(2693),
SampleSum: proto.Float64(1756047.3),
Bucket: []*dto.Bucket{
{
UpperBound: proto.Float64(100),
CumulativeCount: proto.Uint64(123),
},
{
UpperBound: proto.Float64(120),
CumulativeCount: proto.Uint64(412),
Exemplar: &dto.Exemplar{
Label: []*dto.LabelPair{
{
Name: proto.String("foo"),
Value: proto.String("bar"),
},
},
Value: proto.Float64(119.9),
Timestamp: openMetricsTimestamp,
},
},
{
UpperBound: proto.Float64(144),
CumulativeCount: proto.Uint64(592),
Exemplar: &dto.Exemplar{
Label: []*dto.LabelPair{
{
Name: proto.String("foo"),
Value: proto.String("baz"),
},
{
Name: proto.String("dings"),
Value: proto.String("bums"),
},
},
Value: proto.Float64(140.14),
},
},
{
UpperBound: proto.Float64(172.8),
CumulativeCount: proto.Uint64(1524),
},
},
},
},
},
},
out: `# HELP request_duration_microseconds The response latency.
# TYPE request_duration_microseconds histogram
request_duration_microseconds_bucket{le="100.0"} 123
request_duration_microseconds_bucket{le="120.0"} 412 # {foo="bar"} 119.9 12345.6
request_duration_microseconds_bucket{le="144.0"} 592 # {foo="baz",dings="bums"} 140.14
request_duration_microseconds_bucket{le="172.8"} 1524
request_duration_microseconds_bucket{le="+Inf"} 2693
request_duration_microseconds_sum 1.7560473e+06
request_duration_microseconds_count 2693
`,
},
// 10: Simple Counter.
{
in: &dto.MetricFamily{
Name: proto.String("foos_total"),
Help: proto.String("Number of foos."),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(42),
CreatedTimestamp: openMetricsTimestamp,
},
},
},
},
options: []EncoderOption{WithCreatedLines()},
out: `# HELP foos Number of foos.
# TYPE foos counter
foos_total 42.0
foos_created 12345.6
`,
},
// 11: Simple Counter without created line.
{
in: &dto.MetricFamily{
Name: proto.String("foos_total"),
Help: proto.String("Number of foos."),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(42),
CreatedTimestamp: openMetricsTimestamp,
},
},
},
},
out: `# HELP foos Number of foos.
# TYPE foos counter
foos_total 42.0
`,
},
// 12: No metric.
{
in: &dto.MetricFamily{
Name: proto.String("name_total"),
Help: proto.String("doc string"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{},
},
out: `# HELP name doc string
# TYPE name counter
`,
},
// 13: Simple Counter with exemplar that has empty label set:
// ignore the exemplar, since OpenMetrics spec requires labels.
{
in: &dto.MetricFamily{
Name: proto.String("foos_total"),
Help: proto.String("Number of foos."),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(42),
Exemplar: &dto.Exemplar{
Label: []*dto.LabelPair{},
Value: proto.Float64(1),
Timestamp: openMetricsTimestamp,
},
},
},
},
},
out: `# HELP foos Number of foos.
# TYPE foos counter
foos_total 42.0
`,
},
// 14: No metric plus unit.
{
in: &dto.MetricFamily{
Name: proto.String("name_seconds_total"),
Help: proto.String("doc string"),
Type: dto.MetricType_COUNTER.Enum(),
Unit: proto.String("seconds"),
Metric: []*dto.Metric{},
},
options: []EncoderOption{WithUnit()},
out: `# HELP name_seconds doc string
# TYPE name_seconds counter
# UNIT name_seconds seconds
`,
},
// 15: Histogram plus unit, but unit not opted in.
{
in: &dto.MetricFamily{
Name: proto.String("request_duration_microseconds"),
Help: proto.String("The response latency."),
Type: dto.MetricType_HISTOGRAM.Enum(),
Unit: proto.String("microseconds"),
Metric: []*dto.Metric{
{
Histogram: &dto.Histogram{
SampleCount: proto.Uint64(2693),
SampleSum: proto.Float64(1756047.3),
Bucket: []*dto.Bucket{
{
UpperBound: proto.Float64(100),
CumulativeCount: proto.Uint64(123),
},
{
UpperBound: proto.Float64(120),
CumulativeCount: proto.Uint64(412),
},
{
UpperBound: proto.Float64(144),
CumulativeCount: proto.Uint64(592),
},
{
UpperBound: proto.Float64(172.8),
CumulativeCount: proto.Uint64(1524),
},
{
UpperBound: proto.Float64(math.Inf(+1)),
CumulativeCount: proto.Uint64(2693),
},
},
},
},
},
},
out: `# HELP request_duration_microseconds The response latency.
# TYPE request_duration_microseconds histogram
request_duration_microseconds_bucket{le="100.0"} 123
request_duration_microseconds_bucket{le="120.0"} 412
request_duration_microseconds_bucket{le="144.0"} 592
request_duration_microseconds_bucket{le="172.8"} 1524
request_duration_microseconds_bucket{le="+Inf"} 2693
request_duration_microseconds_sum 1.7560473e+06
request_duration_microseconds_count 2693
`,
},
// 16: No metric, unit opted in, no unit in name.
{
in: &dto.MetricFamily{
Name: proto.String("name_total"),
Help: proto.String("doc string"),
Type: dto.MetricType_COUNTER.Enum(),
Unit: proto.String("seconds"),
Metric: []*dto.Metric{},
},
options: []EncoderOption{WithUnit()},
out: `# HELP name_seconds doc string
# TYPE name_seconds counter
# UNIT name_seconds seconds
`,
},
// 17: No metric, unit opted in, BUT unit == nil.
{
in: &dto.MetricFamily{
Name: proto.String("name_total"),
Help: proto.String("doc string"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{},
},
options: []EncoderOption{WithUnit()},
out: `# HELP name doc string
# TYPE name counter
`,
},
// 18: Counter, timestamp given, unit opted in, _total suffix.
{
in: &dto.MetricFamily{
Name: proto.String("some_measure_total"),
Help: proto.String("some testing measurement"),
Type: dto.MetricType_COUNTER.Enum(),
Unit: proto.String("seconds"),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("labelname"),
Value: proto.String("val1"),
},
{
Name: proto.String("basename"),
Value: proto.String("basevalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(42),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("labelname"),
Value: proto.String("val2"),
},
{
Name: proto.String("basename"),
Value: proto.String("basevalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(.23),
},
TimestampMs: proto.Int64(1234567890),
},
},
},
options: []EncoderOption{WithUnit()},
out: `# HELP some_measure_seconds some testing measurement
# TYPE some_measure_seconds counter
# UNIT some_measure_seconds seconds
some_measure_seconds_total{labelname="val1",basename="basevalue"} 42.0
some_measure_seconds_total{labelname="val2",basename="basevalue"} 0.23 1.23456789e+06
`,
},
}
for i, scenario := range scenarios {
out := bytes.NewBuffer(make([]byte, 0, len(scenario.out)))
n, err := MetricFamilyToOpenMetrics(out, scenario.in, scenario.options...)
if err != nil {
t.Errorf("%d. error: %s", i, err)
continue
}
if expected, got := len(scenario.out), n; expected != got {
t.Errorf(
"%d. expected %d bytes written, got %d",
i, expected, got,
)
}
if expected, got := scenario.out, out.String(); expected != got {
t.Errorf(
"%d. expected out=%q, got %q",
i, expected, got,
)
}
}
}
func BenchmarkOpenMetricsCreate(b *testing.B) {
mf := &dto.MetricFamily{
Name: proto.String("request_duration_microseconds"),
Help: proto.String("The response latency."),
Type: dto.MetricType_HISTOGRAM.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("name_1"),
Value: proto.String("val with\nnew line"),
},
{
Name: proto.String("name_2"),
Value: proto.String("val with \\backslash and \"quotes\""),
},
{
Name: proto.String("name_3"),
Value: proto.String("Just a quite long label value to test performance."),
},
},
Histogram: &dto.Histogram{
SampleCount: proto.Uint64(2693),
SampleSum: proto.Float64(1756047.3),
Bucket: []*dto.Bucket{
{
UpperBound: proto.Float64(100),
CumulativeCount: proto.Uint64(123),
},
{
UpperBound: proto.Float64(120),
CumulativeCount: proto.Uint64(412),
},
{
UpperBound: proto.Float64(144),
CumulativeCount: proto.Uint64(592),
},
{
UpperBound: proto.Float64(172.8),
CumulativeCount: proto.Uint64(1524),
},
{
UpperBound: proto.Float64(math.Inf(+1)),
CumulativeCount: proto.Uint64(2693),
},
},
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("name_1"),
Value: proto.String("Björn"),
},
{
Name: proto.String("name_2"),
Value: proto.String("佖佥"),
},
{
Name: proto.String("name_3"),
Value: proto.String("Just a quite long label value to test performance."),
},
},
Histogram: &dto.Histogram{
SampleCount: proto.Uint64(5699),
SampleSum: proto.Float64(49484343543.4343),
Bucket: []*dto.Bucket{
{
UpperBound: proto.Float64(100),
CumulativeCount: proto.Uint64(120),
},
{
UpperBound: proto.Float64(120),
CumulativeCount: proto.Uint64(412),
},
{
UpperBound: proto.Float64(144),
CumulativeCount: proto.Uint64(596),
},
{
UpperBound: proto.Float64(172.8),
CumulativeCount: proto.Uint64(1535),
},
},
},
TimestampMs: proto.Int64(1234567890),
},
},
}
out := bytes.NewBuffer(make([]byte, 0, 1024))
for i := 0; i < b.N; i++ {
_, err := MetricFamilyToOpenMetrics(out, mf)
require.NoError(b, err)
out.Reset()
}
}
func TestOpenMetricsCreateError(t *testing.T) {
scenarios := []struct {
in *dto.MetricFamily
err string
}{
// 0: No metric name.
{
in: &dto.MetricFamily{
Help: proto.String("doc string"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
{
Untyped: &dto.Untyped{
Value: proto.Float64(math.Inf(-1)),
},
},
},
},
err: "MetricFamily has no name",
},
// 1: Wrong type.
{
in: &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("doc string"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Untyped: &dto.Untyped{
Value: proto.Float64(math.Inf(-1)),
},
},
},
},
err: "expected counter in metric",
},
}
for i, scenario := range scenarios {
var out bytes.Buffer
_, err := MetricFamilyToOpenMetrics(&out, scenario.in)
if err == nil {
t.Errorf("%d. expected error, got nil", i)
continue
}
if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 {
t.Errorf(
"%d. expected error starting with %q, got %q",
i, expected, got,
)
}
}
}