From 7d9384021c8b9a8cbb16fd9960190908b4f2be72 Mon Sep 17 00:00:00 2001 From: Iain Buclaw Date: Tue, 19 Jul 2016 20:48:22 +0200 Subject: [PATCH] Fix data rereads and replace _fetchWithBootstrap --- graphite_api/evaluator.py | 7 +- graphite_api/functions.py | 184 +++++++---------- tests/test_functions.py | 409 ++++++++++++++++++++++++++++++++------ 3 files changed, 426 insertions(+), 174 deletions(-) diff --git a/graphite_api/evaluator.py b/graphite_api/evaluator.py index 6cdbf70..7bc39a7 100644 --- a/graphite_api/evaluator.py +++ b/graphite_api/evaluator.py @@ -38,7 +38,11 @@ def evaluateTarget(requestContext, target, data_store=None): return result -def evaluateTokens(requestContext, tokens, data_store): +def evaluateTokens(requestContext, tokens, data_store=None): + if data_store is None: + paths = list(pathsFromTokens(tokens)) + data_store = fetchData(requestContext, paths) + if tokens.expression: return evaluateTokens(requestContext, tokens.expression, data_store) @@ -49,6 +53,7 @@ def evaluateTokens(requestContext, tokens, data_store): func = app.functions[tokens.call.funcname] args = [evaluateTokens(requestContext, arg, data_store) for arg in tokens.call.args] + requestContext['args'] = tokens.call.args kwargs = dict([(kwarg.argname, evaluateTokens(requestContext, kwarg.args[0], diff --git a/graphite_api/functions.py b/graphite_api/functions.py index e25a7c4..4ec315c 100644 --- a/graphite_api/functions.py +++ b/graphite_api/functions.py @@ -27,7 +27,7 @@ from six.moves import zip_longest, map, reduce -from .evaluator import evaluateTarget, pathsFromTarget +from .evaluator import evaluateTarget, evaluateTokens, pathsFromTarget from .render.attime import parseTimeOffset, parseATTime from .render.glyph import format_units from .render.datalib import TimeSeries, fetchData @@ -706,9 +706,7 @@ def movingMedian(requestContext, seriesList, windowSize): Takes one metric or a wildcard seriesList followed by a number N of datapoints or a quoted string with a length of time like '1hour' or '5min' (See ``from / until`` in the render\_api_ for examples of time formats). - Graphs the median of the preceeding datapoints for each point on the - graph. All previous datapoints are set to None at the beginning of the - graph. + Graphs the median of the preceeding datapoints for each point on the graph. Example:: @@ -716,21 +714,27 @@ def movingMedian(requestContext, seriesList, windowSize): &target=movingMedian(Server.instance*.threads.idle,'5min') """ + if not seriesList: + return [] windowInterval = None if isinstance(windowSize, six.string_types): delta = parseTimeOffset(windowSize) windowInterval = to_seconds(delta) if windowInterval: - bootstrapSeconds = windowInterval + previewSeconds = windowInterval else: - bootstrapSeconds = max([s.step for s in seriesList]) * int(windowSize) - - bootstrapList = _fetchWithBootstrap(requestContext, seriesList, - seconds=bootstrapSeconds) + previewSeconds = max([s.step for s in seriesList]) * int(windowSize) + + # ignore original data and pull new, including our preview + # data from earlier is needed to calculate the early results + newContext = requestContext.copy() + newContext['startTime'] = (requestContext['startTime'] - + timedelta(seconds=previewSeconds)) + previewList = evaluateTokens(newContext, requestContext['args'][0]) result = [] - for bootstrap, series in zip_longest(bootstrapList, seriesList): + for series in previewList: if windowInterval: windowPoints = windowInterval // series.step else: @@ -739,14 +743,13 @@ def movingMedian(requestContext, seriesList, windowSize): if isinstance(windowSize, six.string_types): newName = 'movingMedian(%s,"%s")' % (series.name, windowSize) else: - newName = "movingMedian(%s,%d)" % (series.name, windowPoints) - newSeries = TimeSeries(newName, series.start, series.end, series.step, - []) + newName = "movingMedian(%s,%s)" % (series.name, windowSize) + newSeries = TimeSeries(newName, series.start + previewSeconds, + series.end, series.step, []) newSeries.pathExpression = newName - offset = len(bootstrap) - len(series) - for i in range(len(series)): - window = bootstrap[i + offset - windowPoints:i + offset] + for i in range(windowPoints, len(series)): + window = series[i - windowPoints:i] nonNull = [v for v in window if v is not None] if nonNull: m_index = len(nonNull) // 2 @@ -895,7 +898,6 @@ def movingAverage(requestContext, seriesList, windowSize): datapoints or a quoted string with a length of time like '1hour' or '5min' (See ``from / until`` in the render\_api_ for examples of time formats). Graphs the average of the preceeding datapoints for each point on the - graph. All previous datapoints are set to None at the beginning of the graph. Example:: @@ -912,15 +914,19 @@ def movingAverage(requestContext, seriesList, windowSize): windowInterval = to_seconds(delta) if windowInterval: - bootstrapSeconds = windowInterval + previewSeconds = windowInterval else: - bootstrapSeconds = max([s.step for s in seriesList]) * int(windowSize) - - bootstrapList = _fetchWithBootstrap(requestContext, seriesList, - seconds=bootstrapSeconds) + previewSeconds = max([s.step for s in seriesList]) * int(windowSize) + + # ignore original data and pull new, including our preview + # data from earlier is needed to calculate the early results + newContext = requestContext.copy() + newContext['startTime'] = (requestContext['startTime'] - + timedelta(seconds=previewSeconds)) + previewList = evaluateTokens(newContext, requestContext['args'][0]) result = [] - for bootstrap, series in zip_longest(bootstrapList, seriesList): + for series in previewList: if windowInterval: windowPoints = windowInterval // series.step else: @@ -930,13 +936,12 @@ def movingAverage(requestContext, seriesList, windowSize): newName = 'movingAverage(%s,"%s")' % (series.name, windowSize) else: newName = "movingAverage(%s,%s)" % (series.name, windowSize) - newSeries = TimeSeries(newName, series.start, series.end, series.step, - []) + newSeries = TimeSeries(newName, series.start + previewSeconds, + series.end, series.step, []) newSeries.pathExpression = newName - offset = len(bootstrap) - len(series) - for i in range(len(series)): - window = bootstrap[i + offset - windowPoints:i + offset] + for i in range(windowPoints, len(series)): + window = series[i - windowPoints:i] newSeries.append(safeAvg(window)) result.append(newSeries) @@ -2057,83 +2062,6 @@ def secondYAxis(requestContext, seriesList): return seriesList -def _fetchWithBootstrap(requestContext, seriesList, **delta_kwargs): - """ - Request the same data but with a bootstrap period at the beginning. - """ - bootstrapContext = requestContext.copy() - bootstrapContext['startTime'] = ( - requestContext['startTime'] - timedelta(**delta_kwargs)) - bootstrapContext['endTime'] = requestContext['startTime'] - - bootstrapList = [] - - # Get all paths to fetch - paths = [] - for series in seriesList: - if series.pathExpression in [b.pathExpression for b in bootstrapList]: - continue - paths.extend(pathsFromTarget(series.pathExpression)) - - # Fetch all paths - data_store = fetchData(bootstrapContext, paths) - - for series in seriesList: - if series.pathExpression in [b.pathExpression for b in bootstrapList]: - # This pathExpression returns multiple series and we already - # fetched it - continue - bootstraps = evaluateTarget(bootstrapContext, - series.pathExpression, - data_store) - found = dict(((s.name, s) for s in bootstraps)) - for s in seriesList: - if s.name not in found: - # bootstrap interval too large for the range available in - # storage. Fill with nulls. - start = epoch(bootstrapContext['startTime']) - end = epoch(bootstrapContext['endTime']) - delta = (end - start) % s.step - values = [None] * delta - found[s.name] = TimeSeries(s.name, start, end, s.step, values) - found[s.name].pathExpression = s.pathExpression - bootstrapList.append(found[s.name]) - - newSeriesList = [] - for bootstrap, original in zip_longest(bootstrapList, seriesList): - newValues = [] - if bootstrap.step != original.step: - ratio = bootstrap.step / original.step - for value in bootstrap: - # XXX For series with aggregationMethod = sum this should also - # divide by the ratio to bring counts to the same time unit - # ...but we have no way of knowing whether that's the case - newValues.extend([value] * ratio) - else: - newValues.extend(bootstrap) - newValues.extend(original) - - newSeries = TimeSeries(original.name, bootstrap.start, original.end, - original.step, newValues) - newSeries.pathExpression = series.pathExpression - newSeriesList.append(newSeries) - - return newSeriesList - - -def _trimBootstrap(bootstrap, original): - """ - Trim the bootstrap period off the front of this series so it matches the - original. - """ - original_len = len(original) - length_limit = (original_len * original.step) // bootstrap.step - trim_start = bootstrap.end - (length_limit * bootstrap.step) - trimmed = TimeSeries(bootstrap.name, trim_start, bootstrap.end, - bootstrap.step, bootstrap[-length_limit:]) - return trimmed - - def holtWintersIntercept(alpha, actual, last_season, last_intercept, last_slope): return (alpha * (actual - last_season) + @@ -2252,11 +2180,23 @@ def holtWintersForecast(requestContext, seriesList): Performs a Holt-Winters forecast using the series as input data. Data from one week previous to the series is used to bootstrap the initial forecast. """ + previewSeconds = 7 * 86400 # 7 days + # ignore original data and pull new, including our preview + newContext = requestContext.copy() + newContext['startTime'] = (requestContext['startTime'] - + timedelta(seconds=previewSeconds)) + previewList = evaluateTokens(newContext, requestContext['args'][0]) results = [] - bootstrapList = _fetchWithBootstrap(requestContext, seriesList, days=7) - for bootstrap, series in zip_longest(bootstrapList, seriesList): - analysis = holtWintersAnalysis(bootstrap) - results.append(_trimBootstrap(analysis['predictions'], series)) + for series in previewList: + analysis = holtWintersAnalysis(series) + predictions = analysis['predictions'] + windowPoints = previewSeconds // predictions.step + result = TimeSeries("holtWintersForecast(%s)" % series.name, + predictions.start + previewSeconds, + predictions.end, predictions.step, + predictions[windowPoints:]) + result.pathExpression = result.name + results.append(result) return results @@ -2265,12 +2205,28 @@ def holtWintersConfidenceBands(requestContext, seriesList, delta=3): Performs a Holt-Winters forecast using the series as input data and plots upper and lower bands with the predicted forecast deviations. """ + previewSeconds = 7 * 86400 # 7 days + # ignore original data and pull new, including our preview + newContext = requestContext.copy() + newContext['startTime'] = (requestContext['startTime'] - + timedelta(seconds=previewSeconds)) + previewList = evaluateTokens(newContext, requestContext['args'][0]) results = [] - bootstrapList = _fetchWithBootstrap(requestContext, seriesList, days=7) - for bootstrap, series in zip_longest(bootstrapList, seriesList): - analysis = holtWintersAnalysis(bootstrap) - forecast = _trimBootstrap(analysis['predictions'], series) - deviation = _trimBootstrap(analysis['deviations'], series) + for series in previewList: + analysis = holtWintersAnalysis(series) + + data = analysis['predictions'] + windowPoints = previewSeconds // data.step + forecast = TimeSeries(data.name, data.start + previewSeconds, + data.end, data.step, data[windowPoints:]) + forecast.pathExpression = data.pathExpression + + data = analysis['deviations'] + windowPoints = previewSeconds // data.step + deviation = TimeSeries(data.name, data.start + previewSeconds, + data.end, data.step, data[windowPoints:]) + deviation.pathExpression = data.pathExpression + seriesLength = len(forecast) i = 0 upperBand = list() diff --git a/tests/test_functions.py b/tests/test_functions.py index 2e341bf..5fe7cc0 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -451,25 +451,111 @@ def test_weighted_average(self): weight = functions.weightedAverage({}, [series[0]], [series[1]], 0) self.assertEqual(weight[:3], [0, 1, 2]) - def test_moving_median(self): - series = self._generate_series_list() - for s in series: - self.write_series(s) - median = functions.movingMedian({ - 'startTime': parseATTime('-100s') - }, series, '5s')[0] - try: - self.assertEqual(median[:4], [1, 0, 1, 1]) - except AssertionError: # time race condition - self.assertEqual(median[:4], [1, 1, 1, 1]) + def test_moving_median_empty_series(self): + self.assertListEqual(functions.movingMedian({}, [], ""), []) + + def test_moving_median_returns_none(self): + def gen_series_list(start=0): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', + start+10, start+15, 1, range(start, start+15)), + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + series = gen_series_list(10) + + def mock_evaluate(reqCtx, tokens, store=None): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', 10, 25, 1, + [None] * 15) + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + expectedResults = [ + TimeSeries('movingMedian(collectd.test-db0.load.value,60)', + 20, 25, 1, [None, None, None, None, None]) + ] - median = functions.movingMedian({ - 'startTime': parseATTime('-100s') - }, series, 5)[0] - try: - self.assertEqual(median[:4], [1, 0, 1, 1]) - except AssertionError: - self.assertEqual(median[:4], [1, 1, 1, 1]) + with patch('graphite_api.functions.evaluateTokens', mock_evaluate): + result = functions.movingMedian({ + 'args': ({}, {}), + 'startTime': datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 1, 1, 0, 9, 0, 0, pytz.utc), + 'data': [], + }, series, 10) + + self.assertListEqual(result, expectedResults) + + def test_moving_median_returns_empty(self): + def gen_series_list(start=0): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', + start+600, start+700, 1, range(start, start+100)), + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + series = gen_series_list(10) + + def mock_evaluate(reqCtx, tokens, store=None): + return [] + + expectedResults = [] + + with patch('graphite_api.functions.evaluateTokens', mock_evaluate): + result = functions.movingMedian({ + 'args': ({}, {}), + 'startTime': datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 1, 1, 0, 9, 0, 0, pytz.utc), + 'data': [], + }, series, 60) + + self.assertListEqual(result, expectedResults) + + def test_moving_median(self): + def gen_series_list(start=0): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', + start+600, start+700, 1, range(start, start+100)), + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + series = gen_series_list(10) + + def mock_evaluate(reqCtx, tokens, store=None): + return gen_series_list() + + expectedResults = [[ + TimeSeries('movingMedian(collectd.test-db0.load.value,60)', + 660, 700, 1, range(30, 70)), + ], [ + TimeSeries('movingMedian(collectd.test-db0.load.value,"-1min")', + 660, 700, 1, range(30, 70)), + ]] + + with patch('graphite_api.functions.evaluateTokens', mock_evaluate): + result = functions.movingMedian({ + 'args': ({}, {}), + 'startTime': datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 1, 1, 0, 9, 0, 0, pytz.utc), + 'data': [], + }, series, 60) + self.assertListEqual(result, expectedResults[0]) + + result = functions.movingMedian({ + 'args': ({}, {}), + 'startTime': datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 1, 1, 0, 9, 0, 0, pytz.utc), + 'data': [], + }, series, "-1min") + self.assertListEqual(result, expectedResults[1]) def test_invert(self): series = self._generate_series_list() @@ -497,25 +583,116 @@ def test_offset_to_zero(self): offset = functions.offsetToZero({}, series)[0] self.assertEqual(offset[:3], [None, 0, 1]) - def test_moving_average(self): - series = self._generate_series_list() - for s in series: - self.write_series(s) - average = functions.movingAverage({ - 'startTime': parseATTime('-100s') - }, series, '5s')[0] - try: - self.assertEqual(list(average)[:4], [0.5, 1/3., 0.5, 0.8]) - except AssertionError: # time race condition - self.assertEqual(list(average)[:4], [1, 3/4., 0.8, 1.2]) + def test_moving_average_empty_series(self): + self.assertListEqual(functions.movingAverage({}, [], ""), []) + + def test_moving_average_returns_none(self): + def gen_series_list(start=0): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', + start+10, start+15, 1, range(start, start+15)), + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + series = gen_series_list(10) + + def mock_evaluate(reqCtx, tokens, store=None): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', 10, 25, 1, + [None] * 15) + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + expectedResults = [ + TimeSeries('movingAverage(collectd.test-db0.load.value,60)', + 20, 25, 1, [None, None, None, None, None]) + ] - average = functions.movingAverage({ - 'startTime': parseATTime('-100s') - }, series, 5)[0] - try: - self.assertEqual(average[:4], [0.5, 1/3., 0.5, 0.8]) - except AssertionError: - self.assertEqual(list(average)[:4], [1, 3/4., 0.8, 1.2]) + with patch('graphite_api.functions.evaluateTokens', mock_evaluate): + result = functions.movingAverage({ + 'args': ({}, {}), + 'startTime': datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 1, 1, 0, 9, 0, 0, pytz.utc), + 'data': [], + }, series, 10) + + self.assertListEqual(result, expectedResults) + + def test_moving_average_returns_empty(self): + def gen_series_list(start=0): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', + start+600, start+700, 1, range(start, start+100)), + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + series = gen_series_list(10) + + def mock_evaluate(reqCtx, tokens, store=None): + return [] + + expectedResults = [] + + with patch('graphite_api.functions.evaluateTokens', mock_evaluate): + result = functions.movingAverage({ + 'args': ({}, {}), + 'startTime': datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 1, 1, 0, 9, 0, 0, pytz.utc), + 'data': [], + }, series, 60) + + self.assertListEqual(result, expectedResults) + + def test_moving_average(self): + def gen_series_list(start=0): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', + start+600, start+700, 1, range(start, start+100)), + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + series = gen_series_list(10) + + def mock_evaluate(reqCtx, tokens, store=None): + return gen_series_list() + + def frange(x, y, jump): + while x < y: + yield x + x += jump + + expectedResults = [[ + TimeSeries('movingAverage(collectd.test-db0.load.value,60)', + 660, 700, 1, frange(29.5, 69.5, 1)), + ], [ + TimeSeries('movingAverage(collectd.test-db0.load.value,"-1min")', + 660, 700, 1, frange(29.5, 69.5, 1)), + ]] + + with patch('graphite_api.functions.evaluateTokens', mock_evaluate): + result = functions.movingAverage({ + 'args': ({}, {}), + 'startTime': datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 1, 1, 0, 9, 0, 0, pytz.utc), + 'data': [], + }, series, 60) + self.assertListEqual(result, expectedResults[0]) + + result = functions.movingAverage({ + 'args': ({}, {}), + 'startTime': datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 1, 1, 0, 9, 0, 0, pytz.utc), + 'data': [], + }, series, "-1min") + self.assertListEqual(result, expectedResults[1]) def test_cumulative(self): series = self._generate_series_list(config=[range(100)]) @@ -846,33 +1023,147 @@ def test_stdev(self): dev = functions.stdev({}, series, 10)[0] self.assertEqual(dev[1], 0.5) - def test_holt_winters(self): - timespan = 3600 * 24 * 8 # 8 days - stop = int(time.time()) - step = 100 - series = TimeSeries('foo.bar', - stop - timespan, - stop, - step, - [x**1.5 for x in range(0, timespan, step)]) - series[10] = None - series.pathExpression = 'foo.bar' - self.write_series(series, [(100, timespan)]) - - ctx = { - 'startTime': parseATTime('-1d'), + def test_holt_winters_analysis_none(self): + seriesList = TimeSeries('collectd.test-db0.load.value', + 660, 700, 1, [None]) + expectedResults = { + 'predictions': TimeSeries( + 'holtWintersForecast(collectd.test-db0.load.value)', + 660, 700, 1, [None]), + 'deviations': TimeSeries( + 'holtWintersDeviation(collectd.test-db0.load.value)', + 660, 700, 1, [0]), + 'seasonals': [0], + 'slopes': [0], + 'intercepts': [None] } - analysis = functions.holtWintersForecast(ctx, [series]) - self.assertEqual(len(analysis), 1) - analysis = functions.holtWintersConfidenceBands(ctx, [series]) - self.assertEqual(len(analysis), 2) + result = functions.holtWintersAnalysis(seriesList) + self.assertEqual(result, expectedResults) + + def test_holt_winters_forecast(self): + def gen_series_list(start=0): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', + start+600, start+700, 1, range(start, start+100)), + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + series = gen_series_list(10) - analysis = functions.holtWintersConfidenceArea(ctx, [series]) - self.assertEqual(len(analysis), 2) + def mock_evaluate(reqCtx, tokens, store=None): + return gen_series_list() - analysis = functions.holtWintersAberration(ctx, [series]) - self.assertEqual(len(analysis), 1) + expectedResults = [ + TimeSeries('holtWintersForecast(collectd.test-db0.load.value)', + 660, 700, 1, []) + ] + + with patch('graphite_api.functions.evaluateTokens', mock_evaluate): + result = functions.holtWintersForecast({ + 'args': ({}, {}), + 'startTime': datetime(1970, 2, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 2, 1, 0, 9, 0, 0, pytz.utc), + 'data': [], + }, series) + self.assertListEqual(result, expectedResults) + + def test_holt_winters(self): + points = 10 + step = 600 + start_time = 2678400 # 1970-02-01 + week_seconds = 7 * 86400 + + def hw_range(x, y, jump): + while x < y: + yield (x / jump) % 10 + x += jump + + def gen_series_list(start=0, points=10): + seriesList = [ + TimeSeries('collectd.test-db0.load.value', + start, start+(points*step), step, + hw_range(0, points*step, step)), + ] + for series in seriesList: + series.pathExpression = series.name + return seriesList + + series = gen_series_list(start_time, points) + + def mock_evaluate(reqCtx, tokens, store=None): + return gen_series_list(start_time - week_seconds, + (week_seconds / step) + points) + + expectedResults = [[ + TimeSeries( + 'holtWintersConfidenceLower(collectd.test-db0.load.value)', + start_time, start_time+(points*step), step, + [0.2841206166091448, 1.0581027098774411, 0.3338172102994683, + 0.5116859493263242, -0.18199175514936972, 0.2366173792019426, + -1.2941554508809152, -0.513426806531049, -0.7970905542723132, + 0.09868900726536012] + ), + TimeSeries( + 'holtWintersConfidenceUpper(collectd.test-db0.load.value)', + start_time, start_time+(points*step), step, + [8.424944558327624, 9.409422251880809, 10.607070189221787, + 10.288439865038768, 9.491556863132963, 9.474595784593738, + 8.572310478053845, 8.897670449095346, 8.941566968508148, + 9.409728797779282] + ) + ], [ + TimeSeries( + 'holtWintersConfidenceArea(collectd.test-db0.load.value)', + start_time, start_time+(points*step), step, + [0.2841206166091448, 1.0581027098774411, 0.3338172102994683, + 0.5116859493263242, -0.18199175514936972, 0.2366173792019426, + -1.2941554508809152, -0.513426806531049, -0.7970905542723132, + 0.09868900726536012] + ), + TimeSeries( + 'holtWintersConfidenceArea(collectd.test-db0.load.value)', + start_time, start_time+(points*step), step, + [8.424944558327624, 9.409422251880809, 10.607070189221787, + 10.288439865038768, 9.491556863132963, 9.474595784593738, + 8.572310478053845, 8.897670449095346, 8.941566968508148, + 9.409728797779282] + ) + ], [ + TimeSeries( + 'holtWintersAberration(collectd.test-db0.load.value)', + start_time, start_time+(points*step), step, + [-0.2841206166091448, -0.05810270987744115, + 0, 0, 0, 0, 0, 0, 0, 0] + ) + ]] + + with patch('graphite_api.functions.evaluateTokens', mock_evaluate): + result = functions.holtWintersConfidenceBands({ + 'args': ({}, {}), + 'startTime': datetime(1970, 2, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 2, 1, 0, 9, 0, 0, pytz.utc), + 'data': [] + }, series) + self.assertListEqual(result, expectedResults[0]) + + result = functions.holtWintersConfidenceArea({ + 'args': ({}, {}), + 'startTime': datetime(1970, 2, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 2, 1, 0, 9, 0, 0, pytz.utc), + 'data': [] + }, series) + self.assertListEqual(result, expectedResults[1]) + + result = functions.holtWintersAberration({ + 'args': ({}, {}), + 'startTime': datetime(1970, 2, 1, 0, 0, 0, 0, pytz.utc), + 'endTime': datetime(1970, 2, 1, 0, 9, 0, 0, pytz.utc), + 'data': [] + }, series) + self.assertListEqual(result, expectedResults[2]) def test_dashed(self): series = self._generate_series_list(config=[range(100)])