diff --git a/opencensus/metrics/export/metric.py b/opencensus/metrics/export/metric.py new file mode 100644 index 000000000..beb805c54 --- /dev/null +++ b/opencensus/metrics/export/metric.py @@ -0,0 +1,91 @@ +# Copyright 2018, OpenCensus 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. + +from opencensus.metrics.export import metric_descriptor +from opencensus.metrics.export import value + + +DESCRIPTOR_VALUE = { + metric_descriptor.MetricDescriptorType.GAUGE_INT64: + value.ValueLong, + metric_descriptor.MetricDescriptorType.CUMULATIVE_INT64: + value.ValueLong, + metric_descriptor.MetricDescriptorType.GAUGE_DOUBLE: + value.ValueDouble, + metric_descriptor.MetricDescriptorType.CUMULATIVE_DOUBLE: + value.ValueDouble, + metric_descriptor.MetricDescriptorType.GAUGE_DISTRIBUTION: + value.ValueDistribution, + metric_descriptor.MetricDescriptorType.CUMULATIVE_DISTRIBUTION: + value.ValueDistribution, + metric_descriptor.MetricDescriptorType.SUMMARY: + value.ValueSummary, +} + + +class Metric(object): + """A collection of time series data and label metadata. + + This class implements the spec for v1 Metrics as of opencensus-proto + release v0.0.2. See opencensus-proto for details: + + https://github.com/census-instrumentation/opencensus-proto/blob/24333298e36590ea0716598caacc8959fc393c48/src/opencensus/proto/metrics/v1/metrics.proto#33 + + Defines a Metric which has one or more timeseries. + + :type descriptor: class: '~opencensus.metrics.export.metric_descriptor.MetricDescriptor' + :param descriptor: The metric's descriptor. + + :type timeseries: list(:class: '~opencensus.metrics.export.time_series.TimeSeries') + :param timeseries: One or more timeseries for a single metric, where each + timeseries has one or more points. + """ # noqa + + def __init__(self, descriptor, time_series): + if not time_series: + raise ValueError("time_series must not be empty or null") + if descriptor is None: + raise ValueError("descriptor must not be null") + self._time_series = time_series + self._descriptor = descriptor + self._check_type() + + @property + def time_series(self): + return self._time_series + + @property + def descriptor(self): + return self._descriptor + + def _check_type(self): + """Check that point value types match the descriptor type.""" + check_type = DESCRIPTOR_VALUE.get(self.descriptor.type) + if check_type is None: + raise ValueError("Unknown metric descriptor type") + for ts in self.time_series: + if not ts.check_points_type(check_type): + raise ValueError("Invalid point value type") + + def _check_start_timestamp(self): + """Check that starting timestamp exists for cumulative metrics.""" + if self.descriptor.type in ( + metric_descriptor.MetricDescriptorType.CUMULATIVE_INT64, + metric_descriptor.MetricDescriptorType.CUMULATIVE_DOUBLE, + metric_descriptor.MetricDescriptorType.CUMULATIVE_DISTRIBUTION, + ): + for ts in self.time_series: + if ts.start_timestamp is None: + raise ValueError("time_series.start_timestamp must exist " + "for cumulative metrics") diff --git a/opencensus/metrics/export/metric_descriptor.py b/opencensus/metrics/export/metric_descriptor.py index 8f1f6076f..c0c06bc27 100644 --- a/opencensus/metrics/export/metric_descriptor.py +++ b/opencensus/metrics/export/metric_descriptor.py @@ -12,20 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. - import six +from opencensus.metrics.export.value import ValueDistribution +from opencensus.metrics.export.value import ValueSummary + class _MetricDescriptorTypeMeta(type): """Helper for `x in MetricDescriptorType`.""" def __contains__(cls, item): - return item in {MetricDescriptorType.GAUGE_INT64, - MetricDescriptorType.GAUGE_DOUBLE, - MetricDescriptorType.GAUGE_DISTRIBUTION, - MetricDescriptorType.CUMULATIVE_INT64, - MetricDescriptorType.CUMULATIVE_DOUBLE, - MetricDescriptorType.CUMULATIVE_DISTRIBUTION} + return item in { + MetricDescriptorType.GAUGE_INT64, + MetricDescriptorType.GAUGE_DOUBLE, + MetricDescriptorType.GAUGE_DISTRIBUTION, + MetricDescriptorType.CUMULATIVE_INT64, + MetricDescriptorType.CUMULATIVE_DOUBLE, + MetricDescriptorType.CUMULATIVE_DISTRIBUTION + } @six.add_metaclass(_MetricDescriptorTypeMeta) @@ -77,6 +81,22 @@ class MetricDescriptorType(object): # is not recommended, since it cannot be aggregated. SUMMARY = 7 + @classmethod + def to_type_class(cls, metric_descriptor_type): + type_map = { + cls.GAUGE_INT64: int, + cls.GAUGE_DOUBLE: float, + cls.GAUGE_DISTRIBUTION: ValueDistribution, + cls.CUMULATIVE_INT64: int, + cls.CUMULATIVE_DOUBLE: float, + cls.CUMULATIVE_DISTRIBUTION: ValueDistribution, + cls.SUMMARY: ValueSummary + } + try: + return type_map[metric_descriptor_type] + except KeyError: + raise ValueError("Unknown MetricDescriptorType value") + class MetricDescriptor(object): """Defines a metric type and its schema. @@ -105,6 +125,7 @@ class MetricDescriptor(object): :type label_keys: list(:class: '~opencensus.metrics.label_key.LabelKey') :param label_keys: The label keys associated with the metric descriptor. """ + def __init__(self, name, description, unit, type_, label_keys): if type_ not in MetricDescriptorType: raise ValueError("Invalid type") diff --git a/opencensus/metrics/export/time_series.py b/opencensus/metrics/export/time_series.py new file mode 100644 index 000000000..b4dc6fbee --- /dev/null +++ b/opencensus/metrics/export/time_series.py @@ -0,0 +1,74 @@ +# Copyright 2018, OpenCensus 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. + +from opencensus.metrics.export import metric_descriptor + + +class TimeSeries(object): + """Time series data for a given metric and time interval. + + This class implements the spec for v1 TimeSeries structs as of + opencensus-proto release v0.0.2. See opencensus-proto for details: + + https://github.com/census-instrumentation/opencensus-proto/blob/24333298e36590ea0716598caacc8959fc393c48/src/opencensus/proto/metrics/v1/metrics.proto#L112 + + A TimeSeries is a collection of data points that describes the time-varying + values of a metric. + + :type label_values: list(:class: + '~opencensus.metrics.label_value.LabelValue') + :param label_values: The set of label values that uniquely identify this + timeseries. + + :type points: list(:class: '~opencensus.metrics.export.point.Point') + :param points: The data points of this timeseries. + + :type start_timestamp: str + :param start_timestamp: The time when the cumulative value was reset to + zero, must be set for cumulative metrics. + """ # noqa + + def __init__(self, label_values, points, start_timestamp): + if not label_values: + raise ValueError("label_values must not be null or empty") + if not points: + raise ValueError("points must not be null or empty") + self._label_values = label_values + self._points = points + self._start_timestamp = start_timestamp + + @property + def start_timestamp(self): + return self._start_timestamp + + @property + def label_values(self): + return self._label_values + + @property + def points(self): + return self._points + + def check_points_type(self, type_): + """Check that each point's value is an instance `type_`. + + :type type_: type + :param type_: Type to check against. + """ + type_class = ( + metric_descriptor.MetricDescriptorType.to_type_class(type_)) + for point in self.points: + if not isinstance(point.value.value, type_class): + return False + return True diff --git a/opencensus/metrics/export/value.py b/opencensus/metrics/export/value.py index 995abb8f6..1080c4db2 100644 --- a/opencensus/metrics/export/value.py +++ b/opencensus/metrics/export/value.py @@ -24,6 +24,7 @@ class Value(object): Each Point contains exactly one of the four Value types. """ + def __init__(self, value): self._value = value @@ -66,6 +67,7 @@ class ValueDouble(Value): :type value: float :param value: the value in float. """ + def __init__(self, value): super(ValueDouble, self).__init__(value) @@ -76,6 +78,7 @@ class ValueLong(Value): :type value: long :param value: the value in long. """ + def __init__(self, value): super(ValueLong, self).__init__(value) @@ -86,5 +89,149 @@ class ValueSummary(Value): :type value: summary :param value: the value in summary. """ + def __init__(self, value): super(ValueSummary, self).__init__(value) + + +class Exemplar(object): + """An example point to annotate a given value in a bucket. + + Exemplars are example points that may be used to annotate aggregated + Distribution values. They are metadata that gives information about a + particular value added to a Distribution bucket. + + :type value: double + :param value: Value of the exemplar point, determines which bucket the + exemplar belongs to. + + :type timestamp: str + :param timestamp: The observation (sampling) time of the exemplar value. + + :type attachments: dict(str, str) + :param attachments: Contextual information about the example value. + """ + + def __init__(self, value, timestamp, attachments): + self._value = value + self._timestamp = timestamp + self._attachments = attachments + + @property + def value(self): + return self._value + + @property + def timestamp(self): + return self._timestamp + + @property + def attachments(self): + return self._attachments + + +class Bucket(object): + """A bucket of a histogram. + + :type count: int + :param count: The number of values in each bucket of the histogram. + + :type exemplar: Exemplar + :param exemplar: Optional exemplar for this bucket, omit if the + distribution does not have a histogram. + """ + + def __init__(self, count, exemplar): + self._count = count + self._exemplar = exemplar + + @property + def count(self): + return self._count + + @property + def exemplar(self): + return self._exemplar + + +def window(ible, width): + win = [] + for x in ible: + win = (win + [x])[-width:] + if len(win) == width: + yield win + + +class ValueDistribution(Value): + """Summary statistics for a population of values. + + Distribution contains summary statistics for a population of values. It + optionally contains a histogram representing the distribution of those + values across a set of buckets. + + :type count: int + :param count: The number of values in the population. + + :type sum_: float + :param sum_: The sum of the values in the population. + + :type sum_of_squared_deviation: float + :param sum_of_squared_deviation: The sum of squared deviations from the + mean of the values in the population. + + TODO: update to BucketOptions + :type bucket_bounds: list(float) + :param bucket_bounds: Bucket boundaries for the histogram of the values in + the population. + + :type buckets: list(:class: 'Bucket') + :param buckets: Histogram buckets for the given bucket boundaries. + """ + + def __init__(self, count, sum_, sum_of_squared_deviation, bucket_bounds, + buckets): + if count < 0: + raise ValueError("count must be non-negative") + elif count == 0: + if sum_ != 0: + raise ValueError("sum_ must be 0 if count is 0") + if sum_of_squared_deviation != 0: + raise ValueError("sum_of_squared_deviation must be 0 if count " + "is 0") + if not bucket_bounds: + if buckets is not None: + raise ValueError("buckets must be null if bucket_bounds is " + "empty") + else: + for (lo, hi) in window(bucket_bounds, 2): + if lo >= hi: + raise ValueError("bucket_bounds must be monotonically " + "increasing") + if count != sum(bucket.count for bucket in buckets): + raise ValueError("The distribution count must equal the sum " + "of bucket counts") + self._count = count + self._sum = sum_ + self._sum_of_squared_deviation = sum_of_squared_deviation + self._bucket_bounds = bucket_bounds + self._buckets = buckets + + @property + def count(self): + return self._count + + @property + def sum(self): + return self._sum + + @property + def sum_of_squared_deviation(self): + return self._sum_of_squared_deviation + + @property + def bucket_bounds(self): + return self._bucket_bounds + + @property + def buckets(self): + return self._buckets diff --git a/tests/unit/metrics/export/test_metric.py b/tests/unit/metrics/export/test_metric.py new file mode 100644 index 000000000..610f08a22 --- /dev/null +++ b/tests/unit/metrics/export/test_metric.py @@ -0,0 +1,97 @@ +# Copyright 2018, OpenCensus 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. + +try: + from mock import Mock +except ImportError: + from unittest.mock import Mock + +import unittest + +from opencensus.metrics.export import metric +from opencensus.metrics.export import metric_descriptor +from opencensus.metrics.export import time_series + + +class TestMetric(unittest.TestCase): + def test_init(self): + + # Check for required arg errors + with self.assertRaises(ValueError): + metric.Metric(Mock(), None) + with self.assertRaises(ValueError): + metric.Metric(None, Mock()) + + mock_time_series = Mock(spec=time_series.TimeSeries) + mock_time_series.check_points_type.return_value = True + + mock_descriptor = Mock(spec=metric_descriptor.MetricDescriptor) + mock_descriptor.type = (metric_descriptor + .MetricDescriptorType.GAUGE_INT64) + + mm = metric.Metric(mock_descriptor, [mock_time_series],) + self.assertEqual(mm.time_series, [mock_time_series]) + self.assertEqual(mm.descriptor, mock_descriptor) + + def test_init_wrong_ts_type(self): + mock_descriptor = Mock(spec=metric_descriptor.MetricDescriptor) + + mock_time_series1 = Mock(spec=time_series.TimeSeries) + mock_time_series1.check_points_type.return_value = True + + mock_time_series2 = Mock(spec=time_series.TimeSeries) + mock_time_series2.check_points_type.return_value = False + + for value_type in ( + metric_descriptor.MetricDescriptorType.GAUGE_INT64, + metric_descriptor.MetricDescriptorType.CUMULATIVE_INT64, + metric_descriptor.MetricDescriptorType.GAUGE_DOUBLE, + metric_descriptor.MetricDescriptorType.CUMULATIVE_DOUBLE, + metric_descriptor.MetricDescriptorType.GAUGE_DISTRIBUTION, + metric_descriptor.MetricDescriptorType.CUMULATIVE_DISTRIBUTION, + metric_descriptor.MetricDescriptorType.SUMMARY, + 10 # unspecified type + ): + with self.assertRaises(ValueError): + mock_descriptor.type = value_type + metric.Metric(mock_descriptor, + [mock_time_series1, mock_time_series2]) + + def test_init_missing_start_timestamp(self): + mock_ts = Mock(spec=time_series.TimeSeries) + mock_ts.check_points_type.return_value = True + mock_ts_no_start = Mock(spec=time_series.TimeSeries) + mock_ts_no_start.start_timestamp = None + mock_ts_no_start.check_points_type.return_value = True + + mock_descriptor_gauge = Mock(spec=metric_descriptor.MetricDescriptor) + mock_descriptor_gauge.type = ( + metric_descriptor.MetricDescriptorType.GAUGE_INT64 + ) + mock_descriptor_cumulative = ( + Mock(spec=metric_descriptor.MetricDescriptor) + ) + mock_descriptor_cumulative.type = ( + metric_descriptor.MetricDescriptorType.CUMULATIVE_INT64 + ) + + (metric.Metric(mock_descriptor_gauge, [mock_ts]) + ._check_start_timestamp()) + (metric.Metric(mock_descriptor_cumulative, [mock_ts]) + ._check_start_timestamp()) + (metric.Metric(mock_descriptor_gauge, [mock_ts_no_start]) + ._check_start_timestamp()) + with self.assertRaises(ValueError): + metric.Metric(mock_descriptor_cumulative, + [mock_ts_no_start])._check_start_timestamp() diff --git a/tests/unit/metrics/export/test_metric_descriptor.py b/tests/unit/metrics/export/test_metric_descriptor.py index 8b114a50d..2e9b90950 100644 --- a/tests/unit/metrics/export/test_metric_descriptor.py +++ b/tests/unit/metrics/export/test_metric_descriptor.py @@ -20,7 +20,6 @@ from opencensus.metrics.export.metric_descriptor import MetricDescriptorType from opencensus.metrics.label_key import LabelKey - NAME = 'metric' DESCRIPTION = 'Metric description' UNIT = '0.738.[ft_i].[lbf_av]/s' @@ -30,7 +29,6 @@ class TestMetricDescriptor(unittest.TestCase): - def test_init(self): metric_descriptor = MetricDescriptor(NAME, DESCRIPTION, UNIT, MetricDescriptorType.GAUGE_DOUBLE, @@ -45,7 +43,7 @@ def test_init(self): def test_bogus_type(self): with self.assertRaises(ValueError): - MetricDescriptor(NAME, DESCRIPTION, UNIT, 0, (LABEL_KEY1,)) + MetricDescriptor(NAME, DESCRIPTION, UNIT, 0, (LABEL_KEY1, )) def test_null_label_keys(self): with self.assertRaises(ValueError): @@ -55,4 +53,11 @@ def test_null_label_keys(self): def test_null_label_key_values(self): with self.assertRaises(ValueError): MetricDescriptor(NAME, DESCRIPTION, UNIT, - MetricDescriptorType.GAUGE_DOUBLE, (None,)) + MetricDescriptorType.GAUGE_DOUBLE, (None, )) + + def test_to_type_class(self): + self.assertEqual( + MetricDescriptorType.to_type_class( + MetricDescriptorType.GAUGE_INT64), int) + with self.assertRaises(ValueError): + MetricDescriptorType.to_type_class(10) diff --git a/tests/unit/metrics/export/test_point.py b/tests/unit/metrics/export/test_point.py index db11eb3fc..d32ce0357 100644 --- a/tests/unit/metrics/export/test_point.py +++ b/tests/unit/metrics/export/test_point.py @@ -19,7 +19,6 @@ class TestPoint(unittest.TestCase): - def setUp(self): self.double_value = value_module.Value.double_value(55.5) self.long_value = value_module.Value.long_value(9876543210) @@ -29,6 +28,13 @@ def setUp(self): snapshot = summary_module.Snapshot(10, 87.07, value_at_percentile) self.summary = summary_module.Summary(10, 6.6, snapshot) self.summary_value = value_module.Value.summary_value(self.summary) + self.distribution_value = value_module.ValueDistribution( + 100, + 1000.0, + 10.0, + list(range(11)), + [value_module.Bucket(10, None) for ii in range(10)], + ) def test_point_with_double_value(self): point = point_module.Point(self.double_value, self.timestamp) @@ -62,3 +68,13 @@ def test_point_with_summary_value(self): self.assertIsNotNone(point.value) self.assertEqual(point.value, self.summary_value) self.assertEqual(point.value.value, self.summary) + + def test_point_with_distribution_value(self): + point = point_module.Point(self.distribution_value, self.timestamp) + + self.assertIsNotNone(point) + self.assertEqual(point.timestamp, self.timestamp) + + self.assertIsInstance(point.value, value_module.ValueDistribution) + self.assertIsNotNone(point.value) + self.assertEqual(point.value, self.distribution_value) diff --git a/tests/unit/metrics/export/test_time_series.py b/tests/unit/metrics/export/test_time_series.py new file mode 100644 index 000000000..76b475d35 --- /dev/null +++ b/tests/unit/metrics/export/test_time_series.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, OpenCensus 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. + +import unittest + +from opencensus.metrics import label_value +from opencensus.metrics.export import metric_descriptor +from opencensus.metrics.export import point +from opencensus.metrics.export import time_series +from opencensus.metrics.export import value + +START_TIMESTAMP = '2018-10-09T22:33:44.012345Z' +LABEL_VALUE1 = label_value.LabelValue('value one') +LABEL_VALUE2 = label_value.LabelValue('价值二') +LABEL_VALUES = (LABEL_VALUE1, LABEL_VALUE2) +POINTS = (point.Point( + value.Value.long_value(1), "2018-10-09T23:33:44.012345Z"), + point.Point( + value.Value.long_value(2), "2018-10-10T00:33:44.012345Z"), + point.Point( + value.Value.long_value(3), "2018-10-10T01:33:44.012345Z"), + point.Point( + value.Value.long_value(4), "2018-10-10T02:33:44.012345Z"), + point.Point( + value.Value.long_value(5), "2018-10-10T03:33:44.012345Z")) + + +class TestTimeSeries(unittest.TestCase): + def test_init(self): + ts = time_series.TimeSeries(LABEL_VALUES, POINTS, START_TIMESTAMP) + + self.assertEqual(ts.start_timestamp, START_TIMESTAMP) + self.assertEqual(ts.label_values, LABEL_VALUES) + self.assertEqual(ts.points, POINTS) + + def test_init_invalid(self): + time_series.TimeSeries(LABEL_VALUES, POINTS, None) + with self.assertRaises(ValueError): + time_series.TimeSeries(None, POINTS, START_TIMESTAMP) + with self.assertRaises(ValueError): + time_series.TimeSeries([], POINTS, START_TIMESTAMP) + with self.assertRaises(ValueError): + time_series.TimeSeries(LABEL_VALUES, None, START_TIMESTAMP) + with self.assertRaises(ValueError): + time_series.TimeSeries(LABEL_VALUES, [], START_TIMESTAMP) + + def test_check_points_type(self): + ts = time_series.TimeSeries(LABEL_VALUES, POINTS, START_TIMESTAMP) + self.assertTrue( + ts.check_points_type( + metric_descriptor.MetricDescriptorType.GAUGE_INT64)) + + bad_points = POINTS + (point.Point( + value.Value.double_value(6.0), "2018-10-10T04:33:44.012345Z"), ) + bad_time_series = time_series.TimeSeries(LABEL_VALUES, bad_points, + START_TIMESTAMP) + + self.assertFalse( + bad_time_series.check_points_type( + metric_descriptor.MetricDescriptorType.GAUGE_INT64)) + self.assertFalse( + bad_time_series.check_points_type( + metric_descriptor.MetricDescriptorType.GAUGE_DOUBLE)) diff --git a/tests/unit/metrics/export/test_value.py b/tests/unit/metrics/export/test_value.py index d6bfb5fae..b741ece2c 100644 --- a/tests/unit/metrics/export/test_value.py +++ b/tests/unit/metrics/export/test_value.py @@ -13,12 +13,12 @@ # limitations under the License. import unittest + from opencensus.metrics.export import summary as summary_module from opencensus.metrics.export import value as value_module class TestValue(unittest.TestCase): - def test_create_double_value(self): double_value = value_module.Value.double_value(-34.56) @@ -43,3 +43,95 @@ def test_create_summary_value(self): self.assertIsNotNone(summary_value) self.assertIsInstance(summary_value, value_module.ValueSummary) self.assertEqual(summary_value.value, summary) + + +VD_COUNT = 100 +VD_SUM = 1000.0 +VD_SUM_OF_SQUARED_DEVIATION = 10.0 +BUCKET_BOUNDS = list(range(11)) +BUCKETS = [value_module.Bucket(10, None) for ii in range(10)] + + +class TestValueDistribution(unittest.TestCase): + def test_init(self): + distribution = value_module.ValueDistribution( + VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, BUCKET_BOUNDS, + BUCKETS) + self.assertEqual(distribution.count, VD_COUNT) + self.assertEqual(distribution.sum, VD_SUM) + self.assertEqual(distribution.sum_of_squared_deviation, + VD_SUM_OF_SQUARED_DEVIATION) + self.assertEqual(distribution.bucket_bounds, BUCKET_BOUNDS) + self.assertEqual(distribution.buckets, BUCKETS) + + def test_init_no_histogram(self): + distribution = value_module.ValueDistribution( + VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, [], None) + self.assertEqual(distribution.count, VD_COUNT) + self.assertEqual(distribution.sum, VD_SUM) + self.assertEqual(distribution.sum_of_squared_deviation, + VD_SUM_OF_SQUARED_DEVIATION) + self.assertEqual(distribution.bucket_bounds, []) + self.assertEqual(distribution.buckets, None) + + def test_init_bad_args(self): + + with self.assertRaises(ValueError): + value_module.ValueDistribution(-1, VD_SUM, + VD_SUM_OF_SQUARED_DEVIATION, + BUCKET_BOUNDS, BUCKETS) + + with self.assertRaises(ValueError): + value_module.ValueDistribution( + 0, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, BUCKET_BOUNDS, BUCKETS) + + with self.assertRaises(ValueError): + value_module.ValueDistribution(0, 0, VD_SUM_OF_SQUARED_DEVIATION, + BUCKET_BOUNDS, BUCKETS) + + with self.assertRaises(ValueError): + value_module.ValueDistribution( + VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, None, BUCKETS) + + with self.assertRaises(ValueError): + value_module.ValueDistribution( + VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, [], BUCKETS) + + with self.assertRaises(ValueError): + value_module.ValueDistribution(0, 0, 0, BUCKET_BOUNDS, BUCKETS) + + with self.assertRaises(ValueError): + value_module.ValueDistribution( + VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, [1, 1], + [value_module.Bucket(1, None), + value_module.Bucket(1, None)]) + + with self.assertRaises(ValueError): + value_module.ValueDistribution(VD_COUNT - 1, VD_SUM, + VD_SUM_OF_SQUARED_DEVIATION, + BUCKET_BOUNDS, BUCKETS) + + +EX_VALUE = 1.0 +EX_TIMESTAMP = '2018-10-06T17:57:57.936475Z' +EX_ATTACHMENTS = {'attach': 'ments'} + + +class TestExemplar(unittest.TestCase): + def test_init(self): + exemplar = value_module.Exemplar(EX_VALUE, EX_TIMESTAMP, + EX_ATTACHMENTS) + self.assertEqual(exemplar.value, EX_VALUE) + self.assertEqual(exemplar.timestamp, EX_TIMESTAMP) + self.assertEqual(exemplar.attachments, EX_ATTACHMENTS) + + +class TestBucket(unittest.TestCase): + def setUp(self): + self.exemplar = value_module.Exemplar(EX_VALUE, EX_TIMESTAMP, + EX_ATTACHMENTS) + + def test_init(self): + bucket = value_module.Bucket(1, self.exemplar) + self.assertEqual(bucket.count, 1) + self.assertEqual(bucket.exemplar, self.exemplar)