diff --git a/Godeps/_workspace/src/github.com/prometheus/common/model/labels.go b/Godeps/_workspace/src/github.com/prometheus/common/model/labels.go index a73a016c2c..ba694b98ad 100644 --- a/Godeps/_workspace/src/github.com/prometheus/common/model/labels.go +++ b/Godeps/_workspace/src/github.com/prometheus/common/model/labels.go @@ -24,23 +24,23 @@ import ( const ( // ExportedLabelPrefix is the prefix to prepend to the label names present in // exported metrics if a label of the same name is added by the server. - ExportedLabelPrefix LabelName = "exported_" + ExportedLabelPrefix = "exported_" // MetricNameLabel is the label name indicating the metric name of a // timeseries. - MetricNameLabel LabelName = "__name__" + MetricNameLabel = "__name__" // SchemeLabel is the name of the label that holds the scheme on which to // scrape a target. - SchemeLabel LabelName = "__scheme__" + SchemeLabel = "__scheme__" // AddressLabel is the name of the label that holds the address of // a scrape target. - AddressLabel LabelName = "__address__" + AddressLabel = "__address__" // MetricsPathLabel is the name of the label that holds the path on which to // scrape a target. - MetricsPathLabel LabelName = "__metrics_path__" + MetricsPathLabel = "__metrics_path__" // ReservedLabelPrefix is a prefix which is not legal in user-supplied // label names. @@ -63,10 +63,10 @@ const ( // JobLabel is the label name indicating the job from which a timeseries // was scraped. - JobLabel LabelName = "job" + JobLabel = "job" // InstanceLabel is the label name used for the instance label. - InstanceLabel LabelName = "instance" + InstanceLabel = "instance" // BucketLabel is used for the label that defines the upper bound of a // bucket of a histogram ("le" -> "less or equal"). diff --git a/Godeps/_workspace/src/github.com/prometheus/common/model/metric.go b/Godeps/_workspace/src/github.com/prometheus/common/model/metric.go index 34b044386a..25fc3c9425 100644 --- a/Godeps/_workspace/src/github.com/prometheus/common/model/metric.go +++ b/Godeps/_workspace/src/github.com/prometheus/common/model/metric.go @@ -14,7 +14,6 @@ package model import ( - "encoding/json" "fmt" "sort" "strings" @@ -80,41 +79,3 @@ func (m Metric) Fingerprint() Fingerprint { func (m Metric) FastFingerprint() Fingerprint { return LabelSet(m).FastFingerprint() } - -// COWMetric wraps a Metric to enable copy-on-write access patterns. -type COWMetric struct { - Copied bool - Metric Metric -} - -// Set sets a label name in the wrapped Metric to a given value and copies the -// Metric initially, if it is not already a copy. -func (m *COWMetric) Set(ln LabelName, lv LabelValue) { - m.doCOW() - m.Metric[ln] = lv -} - -// Delete deletes a given label name from the wrapped Metric and copies the -// Metric initially, if it is not already a copy. -func (m *COWMetric) Del(ln LabelName) { - m.doCOW() - delete(m.Metric, ln) -} - -// doCOW copies the underlying Metric if it is not already a copy. -func (m *COWMetric) doCOW() { - if !m.Copied { - m.Metric = m.Metric.Clone() - m.Copied = true - } -} - -// String implements fmt.Stringer. -func (m COWMetric) String() string { - return m.Metric.String() -} - -// MarshalJSON implements json.Marshaler. -func (m COWMetric) MarshalJSON() ([]byte, error) { - return json.Marshal(m.Metric) -} diff --git a/Godeps/_workspace/src/github.com/prometheus/common/model/metric_test.go b/Godeps/_workspace/src/github.com/prometheus/common/model/metric_test.go index 29577aca6e..5c7cfceafe 100644 --- a/Godeps/_workspace/src/github.com/prometheus/common/model/metric_test.go +++ b/Godeps/_workspace/src/github.com/prometheus/common/model/metric_test.go @@ -81,52 +81,3 @@ func BenchmarkMetric(b *testing.B) { testMetric(b) } } - -func TestCOWMetric(t *testing.T) { - testMetric := Metric{ - "to_delete": "test1", - "to_change": "test2", - } - - scenarios := []struct { - fn func(*COWMetric) - out Metric - }{ - { - fn: func(cm *COWMetric) { - cm.Del("to_delete") - }, - out: Metric{ - "to_change": "test2", - }, - }, - { - fn: func(cm *COWMetric) { - cm.Set("to_change", "changed") - }, - out: Metric{ - "to_delete": "test1", - "to_change": "changed", - }, - }, - } - - for i, s := range scenarios { - orig := testMetric.Clone() - cm := &COWMetric{ - Metric: orig, - } - - s.fn(cm) - - // Test that the original metric was not modified. - if !orig.Equal(testMetric) { - t.Fatalf("%d. original metric changed; expected %v, got %v", i, testMetric, orig) - } - - // Test that the new metric has the right changes. - if !cm.Metric.Equal(s.out) { - t.Fatalf("%d. copied metric doesn't contain expected changes; expected %v, got %v", i, s.out, cm.Metric) - } - } -} diff --git a/Godeps/_workspace/src/github.com/prometheus/common/model/signature_test.go b/Godeps/_workspace/src/github.com/prometheus/common/model/signature_test.go index e3f1235d28..d9c665f8c7 100644 --- a/Godeps/_workspace/src/github.com/prometheus/common/model/signature_test.go +++ b/Godeps/_workspace/src/github.com/prometheus/common/model/signature_test.go @@ -247,7 +247,7 @@ func BenchmarkMetricToFastFingerprintTriple(b *testing.B) { benchmarkMetricToFastFingerprint(b, LabelSet{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 15738406913934009676) } -func TestEmptyLabelSignature(t *testing.T) { +func BenchmarkEmptyLabelSignature(b *testing.B) { input := []map[string]string{nil, {}} var ms runtime.MemStats @@ -262,7 +262,7 @@ func TestEmptyLabelSignature(t *testing.T) { runtime.ReadMemStats(&ms) if got := ms.Alloc; alloc != got { - t.Fatal("expected LabelsToSignature with empty labels not to perform allocations") + b.Fatal("expected LabelsToSignature with empty labels not to perform allocations") } } diff --git a/Godeps/_workspace/src/github.com/prometheus/common/model/time.go b/Godeps/_workspace/src/github.com/prometheus/common/model/time.go index 43fd0d8082..ebc8bf6cc8 100644 --- a/Godeps/_workspace/src/github.com/prometheus/common/model/time.go +++ b/Godeps/_workspace/src/github.com/prometheus/common/model/time.go @@ -112,9 +112,7 @@ var dotPrecision = int(math.Log10(float64(second))) // String returns a string representation of the Time. func (t Time) String() string { - s := strconv.FormatInt(int64(t), 10) - i := len(s) - dotPrecision - return s[:i] + "." + s[i:] + return strconv.FormatFloat(float64(t)/float64(second), 'f', -1, 64) } // MarshalJSON implements the json.Marshaler interface. diff --git a/Godeps/_workspace/src/github.com/prometheus/common/model/value.go b/Godeps/_workspace/src/github.com/prometheus/common/model/value.go index d32c299a7e..97c49c990b 100644 --- a/Godeps/_workspace/src/github.com/prometheus/common/model/value.go +++ b/Godeps/_workspace/src/github.com/prometheus/common/model/value.go @@ -67,18 +67,13 @@ func (s SamplePair) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return json.Marshal([...]interface{}{t, v}) + return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil } // UnmarshalJSON implements json.Unmarshaler. func (s *SamplePair) UnmarshalJSON(b []byte) error { - if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' { - return fmt.Errorf("sample pair must be array") - } - - b = b[1 : len(b)-1] - - return json.Unmarshal(b, [...]json.Unmarshaler{&s.Timestamp, &s.Value}) + v := [...]json.Unmarshaler{&s.Timestamp, &s.Value} + return json.Unmarshal(b, &v) } // Equal returns true if this SamplePair and o have equal Values and equal @@ -87,15 +82,15 @@ func (s *SamplePair) Equal(o *SamplePair) bool { return s == o || (s.Value == o.Value && s.Timestamp.Equal(o.Timestamp)) } -func (s *SamplePair) String() string { +func (s SamplePair) String() string { return fmt.Sprintf("%s @[%s]", s.Value, s.Timestamp) } // Sample is a sample pair associated with a metric. type Sample struct { - Metric Metric - Value SampleValue - Timestamp Time + Metric Metric `json:"metric"` + Value SampleValue `json:"value"` + Timestamp Time `json:"timestamp"` } // Equal compares first the metrics, then the timestamp, then the value. @@ -117,13 +112,53 @@ func (s *Sample) Equal(o *Sample) bool { return true } -func (s *Sample) String() string { +func (s Sample) String() string { return fmt.Sprintf("%s => %s", s.Metric, SamplePair{ Timestamp: s.Timestamp, Value: s.Value, }) } +// MarshalJSON implements json.Marshaler. +func (s Sample) MarshalJSON() ([]byte, error) { + v := struct { + Metric Metric `json:"metric"` + Value SamplePair `json:"value"` + }{ + Metric: s.Metric, + Value: SamplePair{ + Timestamp: s.Timestamp, + Value: s.Value, + }, + } + + return json.Marshal(&v) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *Sample) UnmarshalJSON(b []byte) error { + v := struct { + Metric Metric `json:"metric"` + Value SamplePair `json:"value"` + }{ + Metric: s.Metric, + Value: SamplePair{ + Timestamp: s.Timestamp, + Value: s.Value, + }, + } + + if err := json.Unmarshal(b, &v); err != nil { + return err + } + + s.Metric = v.Metric + s.Timestamp = v.Value.Timestamp + s.Value = v.Value.Value + + return nil +} + // Samples is a sortable Sample slice. It implements sort.Interface. type Samples []*Sample @@ -169,7 +204,7 @@ type SampleStream struct { Values []SamplePair `json:"values"` } -func (ss *SampleStream) String() string { +func (ss SampleStream) String() string { vals := make([]string, len(ss.Values)) for i, v := range ss.Values { vals[i] = v.String() @@ -247,10 +282,33 @@ type Scalar struct { Timestamp Time } -func (s *Scalar) String() string { +func (s Scalar) String() string { return fmt.Sprintf("scalar: %v @[%v]", s.Value, s.Timestamp) } +// MarshalJSON implements json.Marshaler. +func (s Scalar) MarshalJSON() ([]byte, error) { + v := strconv.FormatFloat(float64(s.Value), 'f', -1, 64) + return json.Marshal([...]interface{}{s.Timestamp, string(v)}) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *Scalar) UnmarshalJSON(b []byte) error { + var f string + v := [...]interface{}{&s.Timestamp, &f} + + if err := json.Unmarshal(b, &v); err != nil { + return err + } + + value, err := strconv.ParseFloat(f, 64) + if err != nil { + return fmt.Errorf("error parsing sample value: %s", err) + } + s.Value = SampleValue(value) + return nil +} + // String is a string value evaluated at the set timestamp. type String struct { Value string @@ -261,6 +319,17 @@ func (s *String) String() string { return s.Value } +// MarshalJSON implements json.Marshaler. +func (s String) MarshalJSON() ([]byte, error) { + return json.Marshal([]interface{}{s.Timestamp, s.Value}) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *String) UnmarshalJSON(b []byte) error { + v := [...]interface{}{&s.Timestamp, &s.Value} + return json.Unmarshal(b, &v) +} + // Vector is basically only an alias for Samples, but the // contract is that in a Vector, all Samples have the same timestamp. type Vector []*Sample diff --git a/Godeps/_workspace/src/github.com/prometheus/common/model/value_test.go b/Godeps/_workspace/src/github.com/prometheus/common/model/value_test.go index 9c9b58e4de..2e9c7eb09d 100644 --- a/Godeps/_workspace/src/github.com/prometheus/common/model/value_test.go +++ b/Godeps/_workspace/src/github.com/prometheus/common/model/value_test.go @@ -14,10 +14,258 @@ package model import ( + "encoding/json" + "math" + "reflect" "sort" "testing" ) +func TestSamplePairJSON(t *testing.T) { + input := []struct { + plain string + value SamplePair + }{ + { + plain: `[1234.567,"123.1"]`, + value: SamplePair{ + Value: 123.1, + Timestamp: 1234567, + }, + }, + } + + for _, test := range input { + b, err := json.Marshal(test.value) + if err != nil { + t.Error(err) + continue + } + + if string(b) != test.plain { + t.Errorf("encoding error: expected %q, got %q", test.plain, b) + continue + } + + var sp SamplePair + err = json.Unmarshal(b, &sp) + if err != nil { + t.Error(err) + continue + } + + if sp != test.value { + t.Errorf("decoding error: expected %v, got %v", test.value, sp) + } + } +} + +func TestSampleJSON(t *testing.T) { + input := []struct { + plain string + value Sample + }{ + { + plain: `{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]}`, + value: Sample{ + Metric: Metric{ + MetricNameLabel: "test_metric", + }, + Value: 123.1, + Timestamp: 1234567, + }, + }, + } + + for _, test := range input { + b, err := json.Marshal(test.value) + if err != nil { + t.Error(err) + continue + } + + if string(b) != test.plain { + t.Errorf("encoding error: expected %q, got %q", test.plain, b) + continue + } + + var sv Sample + err = json.Unmarshal(b, &sv) + if err != nil { + t.Error(err) + continue + } + + if !reflect.DeepEqual(sv, test.value) { + t.Errorf("decoding error: expected %v, got %v", test.value, sv) + } + } +} + +func TestVectorJSON(t *testing.T) { + input := []struct { + plain string + value Vector + }{ + { + plain: `[]`, + value: Vector{}, + }, + { + plain: `[{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]}]`, + value: Vector{&Sample{ + Metric: Metric{ + MetricNameLabel: "test_metric", + }, + Value: 123.1, + Timestamp: 1234567, + }}, + }, + { + plain: `[{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]},{"metric":{"foo":"bar"},"value":[1.234,"+Inf"]}]`, + value: Vector{ + &Sample{ + Metric: Metric{ + MetricNameLabel: "test_metric", + }, + Value: 123.1, + Timestamp: 1234567, + }, + &Sample{ + Metric: Metric{ + "foo": "bar", + }, + Value: SampleValue(math.Inf(1)), + Timestamp: 1234, + }, + }, + }, + } + + for _, test := range input { + b, err := json.Marshal(test.value) + if err != nil { + t.Error(err) + continue + } + + if string(b) != test.plain { + t.Errorf("encoding error: expected %q, got %q", test.plain, b) + continue + } + + var vec Vector + err = json.Unmarshal(b, &vec) + if err != nil { + t.Error(err) + continue + } + + if !reflect.DeepEqual(vec, test.value) { + t.Errorf("decoding error: expected %v, got %v", test.value, vec) + } + } +} + +func TestScalarJSON(t *testing.T) { + input := []struct { + plain string + value Scalar + }{ + { + plain: `[123.456,"456"]`, + value: Scalar{ + Timestamp: 123456, + Value: 456, + }, + }, + { + plain: `[123123.456,"+Inf"]`, + value: Scalar{ + Timestamp: 123123456, + Value: SampleValue(math.Inf(1)), + }, + }, + { + plain: `[123123.456,"-Inf"]`, + value: Scalar{ + Timestamp: 123123456, + Value: SampleValue(math.Inf(-1)), + }, + }, + } + + for _, test := range input { + b, err := json.Marshal(test.value) + if err != nil { + t.Error(err) + continue + } + + if string(b) != test.plain { + t.Errorf("encoding error: expected %q, got %q", test.plain, b) + continue + } + + var sv Scalar + err = json.Unmarshal(b, &sv) + if err != nil { + t.Error(err) + continue + } + + if sv != test.value { + t.Errorf("decoding error: expected %v, got %v", test.value, sv) + } + } +} + +func TestStringJSON(t *testing.T) { + input := []struct { + plain string + value String + }{ + { + plain: `[123.456,"test"]`, + value: String{ + Timestamp: 123456, + Value: "test", + }, + }, + { + plain: `[123123.456,"台北"]`, + value: String{ + Timestamp: 123123456, + Value: "台北", + }, + }, + } + + for _, test := range input { + b, err := json.Marshal(test.value) + if err != nil { + t.Error(err) + continue + } + + if string(b) != test.plain { + t.Errorf("encoding error: expected %q, got %q", test.plain, b) + continue + } + + var sv String + err = json.Unmarshal(b, &sv) + if err != nil { + t.Error(err) + continue + } + + if sv != test.value { + t.Errorf("decoding error: expected %v, got %v", test.value, sv) + } + } +} + func TestVectorSort(t *testing.T) { input := Vector{ &Sample{ diff --git a/storage/local/interface.go b/storage/local/interface.go index bf0b356d90..3e77e4d957 100644 --- a/storage/local/interface.go +++ b/storage/local/interface.go @@ -40,7 +40,7 @@ type Storage interface { // MetricsForLabelMatchers returns the metrics from storage that satisfy the given // label matchers. At least one label matcher must be specified that does not // match the empty string. - MetricsForLabelMatchers(...*metric.LabelMatcher) map[model.Fingerprint]model.COWMetric + MetricsForLabelMatchers(...*metric.LabelMatcher) map[model.Fingerprint]metric.Metric // LastSamplePairForFingerprint returns the last sample pair for the // provided fingerprint. If the respective time series does not exist or // has an evicted head chunk, nil is returned. @@ -48,7 +48,7 @@ type Storage interface { // Get all of the label values that are associated with a given label name. LabelValuesForLabelName(model.LabelName) model.LabelValues // Get the metric associated with the provided fingerprint. - MetricForFingerprint(model.Fingerprint) model.COWMetric + MetricForFingerprint(model.Fingerprint) metric.Metric // Construct an iterator for a given fingerprint. // The iterator will never return samples older than retention time, // relative to the time NewIterator was called. diff --git a/storage/local/storage.go b/storage/local/storage.go index 7883641df0..7c1ae97269 100644 --- a/storage/local/storage.go +++ b/storage/local/storage.go @@ -410,7 +410,7 @@ func (s *memorySeriesStorage) fingerprintsForLabelPairs(pairs ...model.LabelPair } // MetricsForLabelMatchers implements Storage. -func (s *memorySeriesStorage) MetricsForLabelMatchers(matchers ...*metric.LabelMatcher) map[model.Fingerprint]model.COWMetric { +func (s *memorySeriesStorage) MetricsForLabelMatchers(matchers ...*metric.LabelMatcher) map[model.Fingerprint]metric.Metric { var ( equals []model.LabelPair filters []*metric.LabelMatcher @@ -462,7 +462,7 @@ func (s *memorySeriesStorage) MetricsForLabelMatchers(matchers ...*metric.LabelM filters = remaining } - result := make(map[model.Fingerprint]model.COWMetric, len(resFPs)) + result := make(map[model.Fingerprint]metric.Metric, len(resFPs)) for fp := range resFPs { result[fp] = s.MetricForFingerprint(fp) } @@ -486,7 +486,7 @@ func (s *memorySeriesStorage) LabelValuesForLabelName(labelName model.LabelName) } // MetricForFingerprint implements Storage. -func (s *memorySeriesStorage) MetricForFingerprint(fp model.Fingerprint) model.COWMetric { +func (s *memorySeriesStorage) MetricForFingerprint(fp model.Fingerprint) metric.Metric { s.fpLocker.Lock(fp) defer s.fpLocker.Unlock(fp) @@ -494,16 +494,18 @@ func (s *memorySeriesStorage) MetricForFingerprint(fp model.Fingerprint) model.C if ok { // Wrap the returned metric in a copy-on-write (COW) metric here because // the caller might mutate it. - return model.COWMetric{ + return metric.Metric{ Metric: series.metric, } } - metric, err := s.persistence.archivedMetric(fp) + met, err := s.persistence.archivedMetric(fp) if err != nil { log.Errorf("Error retrieving archived metric for fingerprint %v: %v", fp, err) } - return model.COWMetric{ - Metric: metric, + + return metric.Metric{ + Metric: met, + Copied: false, } } diff --git a/storage/local/storage_test.go b/storage/local/storage_test.go index f7ed9a9351..a3f11de651 100644 --- a/storage/local/storage_test.go +++ b/storage/local/storage_test.go @@ -276,7 +276,7 @@ func TestFingerprintsForLabels(t *testing.T) { } } -var benchLabelMatchingRes map[model.Fingerprint]model.COWMetric +var benchLabelMatchingRes map[model.Fingerprint]metric.Metric func BenchmarkLabelMatching(b *testing.B) { s, closer := NewTestStorage(b, 1) @@ -359,7 +359,7 @@ func BenchmarkLabelMatching(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - benchLabelMatchingRes = map[model.Fingerprint]model.COWMetric{} + benchLabelMatchingRes = map[model.Fingerprint]metric.Metric{} for _, mt := range matcherTests { benchLabelMatchingRes = s.MetricsForLabelMatchers(mt...) } diff --git a/storage/metric/metric.go b/storage/metric/metric.go new file mode 100644 index 0000000000..7328ac7a88 --- /dev/null +++ b/storage/metric/metric.go @@ -0,0 +1,63 @@ +// Copyright 2014 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 metric + +import "github.com/prometheus/common/model" + +// Metric wraps a model.Metric and copies it upon modification if Copied is false. +type Metric struct { + Copied bool + Metric model.Metric +} + +// Set sets a label name in the wrapped Metric to a given value and copies the +// Metric initially, if it is not already a copy. +func (m *Metric) Set(ln model.LabelName, lv model.LabelValue) { + m.Copy() + m.Metric[ln] = lv +} + +// Del deletes a given label name from the wrapped Metric and copies the +// Metric initially, if it is not already a copy. +func (m *Metric) Del(ln model.LabelName) { + m.Copy() + delete(m.Metric, ln) +} + +// Get the value for the given label name. An empty value is returned +// if the label does not exist in the metric. +func (m *Metric) Get(ln model.LabelName) model.LabelValue { + return m.Metric[ln] +} + +// Gets behaves as Get but the returned boolean is false iff the label +// does not exist. +func (m *Metric) Gets(ln model.LabelName) (model.LabelValue, bool) { + lv, ok := m.Metric[ln] + return lv, ok +} + +// Copy the underlying Metric if it is not already a copy. +func (m *Metric) Copy() *Metric { + if !m.Copied { + m.Metric = m.Metric.Clone() + m.Copied = true + } + return m +} + +// String implements fmt.Stringer. +func (m Metric) String() string { + return m.Metric.String() +} diff --git a/storage/metric/metric_test.go b/storage/metric/metric_test.go new file mode 100644 index 0000000000..855bdcb2d7 --- /dev/null +++ b/storage/metric/metric_test.go @@ -0,0 +1,70 @@ +// Copyright 2014 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 metric + +import ( + "testing" + + "github.com/prometheus/common/model" +) + +func TestMetric(t *testing.T) { + testMetric := model.Metric{ + "to_delete": "test1", + "to_change": "test2", + } + + scenarios := []struct { + fn func(*Metric) + out Metric + }{ + { + fn: func(cm *etric) { + cm.Del("to_delete") + }, + out: Metric{ + "to_change": "test2", + }, + }, + { + fn: func(cm *COWMetric) { + cm.Set("to_change", "changed") + }, + out: Metric{ + "to_delete": "test1", + "to_change": "changed", + }, + }, + } + + for i, s := range scenarios { + orig := testMetric.Clone() + cm := &Metric{ + Metric: orig, + Copied: false, + } + + s.fn(cm) + + // Test that the original metric was not modified. + if !orig.Equal(testMetric) { + t.Fatalf("%d. original metric changed; expected %v, got %v", i, testMetric, orig) + } + + // Test that the new metric has the right changes. + if !cm.Metric.Equal(s.out) { + t.Fatalf("%d. copied metric doesn't contain expected changes; expected %v, got %v", i, s.out, cm.Metric) + } + } +}