diff --git a/README.md b/README.md index 4ec21ef..d70b435 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,8 @@ lua_shared_dict metrix 16m; # creating metrix storage in nginx shared memory. Us init_by_lua_block { metrix = require 'nginx-metrix.main'({ shared_dict = 'metrix', - vhosts = {'mydomain1', 'mydomain2', ...} + vhosts = {'mydomain1', 'mydomain2', ...}, + window_size = 10, }) } diff --git a/nginx-metrix/collectors.lua b/nginx-metrix/collectors.lua index 29d9bbf..f7fe46d 100644 --- a/nginx-metrix/collectors.lua +++ b/nginx-metrix/collectors.lua @@ -3,7 +3,7 @@ local storage_collector_wrapper_factory = require 'nginx-metrix.storage.collecto local exports = {} -local collectors = {{name = 'dummy'}} -- dummy item for correct work iter function +local collectors = { { name = 'dummy' } } -- dummy item for correct work iter function local collectors_iter = iter(collectors) table.remove(collectors, 1) -- removing dummy item @@ -12,53 +12,53 @@ table.remove(collectors, 1) -- removing dummy item -- @return bool --- local collector_exists = function(collector) - return index(collector.name, collectors_iter:map(function(c) return c.name end)) ~= nil + return index(collector.name, collectors_iter:map(function(c) return c.name end)) ~= nil end --- -- @param collector table --- local collector_validate = function(collector) - assert( - type(collector) == 'table', - ('Collector MUST be a table, got %s: %s'):format(type(collector), inspect(collector)) - ) - - assert( - type(collector.name) == 'string', - ('Collector must have string property "name", got %s: %s'):format(type(collector.name), inspect(collector)) - ) - - -- collector exists - assert( - not collector_exists(collector), - ('Collector<%s> already exists'):format(collector.name) - ) - - assert( - type(collector.ngx_phases) == 'table', - ('Collector<%s>.ngx_phases must be an array, given: %s'):format(collector.name, type(collector.ngx_phases)) - ) - - assert( - all(function(phase) return type(phase) == 'string' end, collector.ngx_phases), - ('Collector<%s>.ngx_phases must be an array of strings, given: %s'):format(collector.name, inspect(collector.ngx_phases)) - ) - - assert( - is.callable(collector.on_phase), - ('Collector<%s>:on_phase must be a function or callable table, given: %s'):format(collector.name, type(collector.on_phase)) - ) - - assert( - type(collector.fields) == 'table', - ('Collector<%s>.fields must be a table, given: %s'):format(collector.name, type(collector.fields)) - ) - - assert( - all(function(field, params) return type(field) == 'string' and type(params) == 'table' end, collector.fields), - ('Collector<%s>.fields must be an table[string, table], given: %s'):format(collector.name, inspect(collector.fields)) - ) + assert( + type(collector) == 'table', + ('Collector MUST be a table, got %s: %s'):format(type(collector), inspect(collector)) + ) + + assert( + type(collector.name) == 'string', + ('Collector must have string property "name", got %s: %s'):format(type(collector.name), inspect(collector)) + ) + + -- collector exists + assert( + not collector_exists(collector), + ('Collector<%s> already exists'):format(collector.name) + ) + + assert( + type(collector.ngx_phases) == 'table', + ('Collector<%s>.ngx_phases must be an array, given: %s'):format(collector.name, type(collector.ngx_phases)) + ) + + assert( + all(function(phase) return type(phase) == 'string' end, collector.ngx_phases), + ('Collector<%s>.ngx_phases must be an array of strings, given: %s'):format(collector.name, inspect(collector.ngx_phases)) + ) + + assert( + is.callable(collector.on_phase), + ('Collector<%s>:on_phase must be a function or callable table, given: %s'):format(collector.name, type(collector.on_phase)) + ) + + assert( + type(collector.fields) == 'table', + ('Collector<%s>.fields must be a table, given: %s'):format(collector.name, type(collector.fields)) + ) + + assert( + all(function(field, params) return type(field) == 'string' and type(params) == 'table' end, collector.fields), + ('Collector<%s>.fields must be an table[string, table], given: %s'):format(collector.name, inspect(collector.fields)) + ) end --- @@ -66,44 +66,39 @@ end -- @return table --- local collector_extend = function(collector) - local _metatable = { - init = function(self, storage) - self.storage = storage - end, - - handle_ngx_phase = function(self, phase) - self:on_phase(phase) - end, - - aggregate = function(self) - iter(self.fields):each(function(field, params) - if params.mean then - self.storage:mean_flush(field) - elseif params.cyclic then - self.storage:cyclic_flush(field) - end - end) - end, - - get_raw_stats = function(self) - return iter(self.fields):map(function(k, _) - return k, (self.storage:get(k) or 0) - end) - end, - - get_text_stats = function(self, output_helper) - return output_helper.render_stats(self) - end, - - get_html_stats = function(self, output_helper) - return output_helper.render_stats(self) + local _metatable = { + init = function(self, storage) + self.storage = storage + end, + handle_ngx_phase = function(self, phase) + self:on_phase(phase) + end, + aggregate = function(self) + iter(self.fields):each(function(field, params) + if params.mean then + self.storage:mean_flush(field) + elseif params.cyclic then + self.storage:cyclic_flush(field, params.window) end - } - _metatable.__index = _metatable - - setmetatable(collector, _metatable) - - return collector + end) + end, + get_raw_stats = function(self) + return iter(self.fields):map(function(k, _) + return k, (self.storage:get(k) or 0) + end) + end, + get_text_stats = function(self, output_helper) + return output_helper.render_stats(self) + end, + get_html_stats = function(self, output_helper) + return output_helper.render_stats(self) + end + } + _metatable.__index = _metatable + + setmetatable(collector, _metatable) + + return collector end --- @@ -111,38 +106,39 @@ end -- @return table --- local collector_register = function(collector) - collector_validate(collector) + collector_validate(collector) - collector = collector_extend(collector) + collector = collector_extend(collector) - local storage = storage_collector_wrapper_factory.create(collector) - collector:init(storage) + local storage = storage_collector_wrapper_factory.create(collector) + collector:init(storage) - table.insert(collectors, collector) + table.insert(collectors, collector) - return collector + return collector end -------------------------------------------------------------------------------- -- EXPORTS -------------------------------------------------------------------------------- exports.register = collector_register -exports.all = collectors_iter +exports.all = collectors_iter +exports.set_window_size = storage_collector_wrapper_factory.set_window_size if __TEST__ then - exports.__private__ = { - collectors = function(value) - if value ~= nil then - local count = length(collectors) - while count > 0 do table.remove(collectors); count = count - 1 end - iter(value):each(function(collector) table.insert(collectors, collector) end) - end - return collectors - end, - collector_exists = collector_exists, - collector_extend = collector_extend, - collector_validate = collector_validate, - } + exports.__private__ = { + collectors = function(value) + if value ~= nil then + local count = length(collectors) + while count > 0 do table.remove(collectors); count = count - 1 end + iter(value):each(function(collector) table.insert(collectors, collector) end) + end + return collectors + end, + collector_exists = collector_exists, + collector_extend = collector_extend, + collector_validate = collector_validate, + } end return exports diff --git a/nginx-metrix/collectors/request.lua b/nginx-metrix/collectors/request.lua index f90eedf..c70254f 100644 --- a/nginx-metrix/collectors/request.lua +++ b/nginx-metrix/collectors/request.lua @@ -1,23 +1,23 @@ local collector = { - name = 'request', - ngx_phases = { [[log]] }, - fields = { - rps = { format = '%d', cyclic = true, }, - internal_rps = { format = '%d', cyclic = true, }, - https_rps = { format = '%d', cyclic = true, }, - time_ps = { format = '%0.3f', mean = true, }, - length_ps = { format = '%0.3f', mean = true, }, - } + name = 'request', + ngx_phases = { [[log]] }, + fields = { + rps = { format = '%d', cyclic = true, window = true, }, + internal_rps = { format = '%d', cyclic = true, window = true, }, + https_rps = { format = '%d', cyclic = true, window = true, }, + time_ps = { format = '%0.3f', mean = true, }, + length_ps = { format = '%0.3f', mean = true, }, + } } function collector:on_phase(phase) - if phase == 'log' then - self.storage:cyclic_incr('rps') - if tonumber(ngx.var.request_time) ~= nil then self.storage:mean_add('time_ps', ngx.var.request_time) end - if ngx.req.is_internal() then self.storage:cyclic_incr('internal_rps') end - if ngx.var.https == 'on' then self.storage:cyclic_incr('https_rps') end - if tonumber(ngx.var.request_length) ~= nil then self.storage:mean_add('length_ps', ngx.var.request_length) end - end + if phase == 'log' then + self.storage:cyclic_incr('rps') + if tonumber(ngx.var.request_time) ~= nil then self.storage:mean_add('time_ps', ngx.var.request_time) end + if ngx.req.is_internal() then self.storage:cyclic_incr('internal_rps') end + if ngx.var.https == 'on' then self.storage:cyclic_incr('https_rps') end + if tonumber(ngx.var.request_length) ~= nil then self.storage:mean_add('length_ps', ngx.var.request_length) end + end end return collector diff --git a/nginx-metrix/collectors/status.lua b/nginx-metrix/collectors/status.lua index ee06311..2a05b13 100644 --- a/nginx-metrix/collectors/status.lua +++ b/nginx-metrix/collectors/status.lua @@ -6,31 +6,31 @@ -- 426, 428, 429, 431, 451, 499, -- 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, 599, -local field_params = { format = '%d', cyclic = true, } +local field_params = { format = '%d', cyclic = true, window = true, } local collector = { - name = 'status', - fields = { - ['200'] = field_params, - ['301'] = field_params, - ['302'] = field_params, - ['304'] = field_params, - ['403'] = field_params, - ['404'] = field_params, - ['500'] = field_params, - ['502'] = field_params, - ['503'] = field_params, - ['504'] = field_params, - }, - ngx_phases = {[[log]]}, - on_phase = function(self, phase) - if phase == 'log' and ngx.status ~= nil then - if self.fields[tostring(ngx.status)] == nil then - self.fields[tostring(ngx.status)] = field_params - end - self.storage:cyclic_incr(ngx.status) - end - end, + name = 'status', + fields = { + ['200'] = field_params, + ['301'] = field_params, + ['302'] = field_params, + ['304'] = field_params, + ['403'] = field_params, + ['404'] = field_params, + ['500'] = field_params, + ['502'] = field_params, + ['503'] = field_params, + ['504'] = field_params, + }, + ngx_phases = { [[log]] }, + on_phase = function(self, phase) + if phase == 'log' and ngx.status ~= nil then + if self.fields[tostring(ngx.status)] == nil then + self.fields[tostring(ngx.status)] = field_params + end + self.storage:cyclic_incr(ngx.status) + end + end, } return collector diff --git a/nginx-metrix/collectors/upstream.lua b/nginx-metrix/collectors/upstream.lua index cffb5b9..7616934 100644 --- a/nginx-metrix/collectors/upstream.lua +++ b/nginx-metrix/collectors/upstream.lua @@ -1,21 +1,21 @@ local collector = { - name = 'upstream', - ngx_phases = { [[log]] }, - fields = { - rps = { format = '%d', cyclic = true, }, - connect_time = { format = '%0.3f', mean = true, }, - header_time = { format = '%0.3f', mean = true, }, - response_time = { format = '%0.3f', mean = true, }, - } + name = 'upstream', + ngx_phases = { [[log]] }, + fields = { + rps = { format = '%d', cyclic = true, window = true, }, + connect_time = { format = '%0.3f', mean = true, }, + header_time = { format = '%0.3f', mean = true, }, + response_time = { format = '%0.3f', mean = true, }, + } } function collector:on_phase(phase) - if phase == 'log' and ngx.var.upstream_addr ~= nil then - self.storage:cyclic_incr('rps') - if tonumber(ngx.var.upstream_connect_time) ~= nil then self.storage:mean_add('connect_time', ngx.var.upstream_connect_time) end - if tonumber(ngx.var.upstream_header_time) ~= nil then self.storage:mean_add('header_time', ngx.var.upstream_header_time) end - if tonumber(ngx.var.upstream_response_time) ~= nil then self.storage:mean_add('response_time', ngx.var.upstream_response_time) end - end + if phase == 'log' and ngx.var.upstream_addr ~= nil then + self.storage:cyclic_incr('rps') + if tonumber(ngx.var.upstream_connect_time) ~= nil then self.storage:mean_add('connect_time', ngx.var.upstream_connect_time) end + if tonumber(ngx.var.upstream_header_time) ~= nil then self.storage:mean_add('header_time', ngx.var.upstream_header_time) end + if tonumber(ngx.var.upstream_response_time) ~= nil then self.storage:mean_add('response_time', ngx.var.upstream_response_time) end + end end return collector diff --git a/nginx-metrix/main.lua b/nginx-metrix/main.lua index f7a0c35..5897ce8 100644 --- a/nginx-metrix/main.lua +++ b/nginx-metrix/main.lua @@ -107,6 +107,8 @@ setmetatable(exports, { namespaces.init({namespaces=options.vhosts}) end + collectors.set_window_size(options.window_size) + if not options.skip_register_builtin_collectors then self:register_builtin_collectors() end @@ -115,4 +117,4 @@ setmetatable(exports, { end, }) -return exports \ No newline at end of file +return exports diff --git a/nginx-metrix/scheduler.lua b/nginx-metrix/scheduler.lua index ecf77fc..d097a1f 100644 --- a/nginx-metrix/scheduler.lua +++ b/nginx-metrix/scheduler.lua @@ -90,11 +90,7 @@ end --- -- @return bool local start = function() - local worker_id, incr_err = dict.safe_incr('worker_id') - if incr_err ~= nil then - logger.error('Can not make worker_id', incr_err) - return false - end + local worker_id = ngx.worker.id() logger.debug(('[scheduler #%s] starting'):format(worker_id)) diff --git a/nginx-metrix/storage/collector_wrapper_factory.lua b/nginx-metrix/storage/collector_wrapper_factory.lua index 3e8b067..7691761 100644 --- a/nginx-metrix/storage/collector_wrapper_factory.lua +++ b/nginx-metrix/storage/collector_wrapper_factory.lua @@ -1,9 +1,12 @@ local namespaces = require 'nginx-metrix.storage.namespaces' local storage_dict = require 'nginx-metrix.storage.dict' +local Window = require 'nginx-metrix.storage.window' local key_sep_namespace = 'ː' local key_sep_collector = '¦' +local global_window_size + --- -- @param str -- @return string @@ -174,11 +177,24 @@ end --- -- @param key string --- -wrapper_metatable.cyclic_flush = function(self, key) +wrapper_metatable.cyclic_flush = function(self, key, use_window) key = self:prepare_key(key) local next_key = key .. '^^next^^' local next_value = storage_dict.get(next_key) or 0 storage_dict.delete(next_key) + + if use_window then + local window_size = use_window + if window_size == true then + window_size = global_window_size + end + if window_size ~= nil and window_size > 1 then + local window = Window.open(key, window_size) + window:push(next_value) + next_value = sum(window:totable()) / window:size() + end + end + storage_dict.set(key, next_value, 0, 0) end @@ -196,12 +212,21 @@ local create = function(collector) return wrapper end +--- +-- @param window_size +-- +local set_window_size = function(window_size) + assert(window_size == nil or window_size > 0, 'window_size can be nil or int grater then 0') + global_window_size = window_size +end + -------------------------------------------------------------------------------- -- EXPORTS -------------------------------------------------------------------------------- local exports = {} exports.create = create +exports.set_window_size = set_window_size if __TEST__ then exports.__private__ = { diff --git a/nginx-metrix/storage/window.lua b/nginx-metrix/storage/window.lua new file mode 100644 index 0000000..0373a45 --- /dev/null +++ b/nginx-metrix/storage/window.lua @@ -0,0 +1,176 @@ +local math = require 'math' +local storage_dict = require 'nginx-metrix.storage.dict' + +-- Window Item -- +local WindowItem = {} +WindowItem.__index = WindowItem + +--- +-- +function WindowItem.create_key() + math.randomseed(ngx.now()) + return ngx.md5(string.format('%.3f %.12f', ngx.now(), math.random())) +end + +--- +-- @param payload +-- +function WindowItem.new(payload) + local windowItem = setmetatable({ _key = WindowItem.create_key(), _payload = payload, _next = nil }, WindowItem) + windowItem:store() + return windowItem +end + +--- +-- @param self WindowItem +-- +function WindowItem.store(self) + return storage_dict.set('WindowItem^' .. self._key, self) +end + +--- +-- @param key +-- +function WindowItem.restore(key) + local data = storage_dict.get('WindowItem^' .. key) + if data ~= nil then + return setmetatable(data, WindowItem) + end + return nil +end + +--- +-- @param self +-- +function WindowItem.key(self) + return self._key +end + +function WindowItem.payload(self) + return self._payload +end + +--- +-- @param self +-- @param next +-- +function WindowItem.next(self, next) + if next ~= nil then + self._next = next:key() + self:store() + elseif self._next == nil then + return nil + else + next = WindowItem.restore(self._next) + + if next == nil then + self._next = nil + self:store() + end + + return next + end +end + +-- /Window Item -- + +-- Window -- +local Window = {} +Window.__index = Window + +--- +-- @param name +-- @param limit +-- +function Window.open(name, limit) + local window = (storage_dict.get('Window^' .. name)) or { _name = name, _limit = limit, _size = 0, _head = nil, _tail = nil } + return setmetatable(window, Window) +end + +--- +-- @param self +-- +function Window.store(self) + return storage_dict.set('Window^' .. self._name, self) +end + +function Window.size(self) + return self._size +end + +Window.len = Window.size + +--- +-- @param self +-- @param payload +-- +function Window.push(self, payload) + if self._limit == self._size then + self:pop() + end + + local item = WindowItem.new(payload) + + if self._tail ~= nil then + WindowItem.restore(self._tail):next(item) + end + + self._tail = item:key() + if self._head == nil then + self._head = self._tail + end + self._size = self._size + 1 + + self:store() +end + +--- +-- @param self +-- +function Window.pop(self) + if self._head == nil then + return nil + end + + local head = WindowItem.restore(self._head) + self._head = head._next + if self._head == nil then + self._tail = nil + end + self._size = self._size - 1 + + self:store() + + return head:payload() +end + +--- +-- @param self +-- +function Window.totable(self) + local list = {} + + local item = self._head and WindowItem.restore(self._head) + while item ~= nil do + table.insert(list, item:payload()) + item = item:next() + end + + return list; +end + +-- /Window -- + +-------------------------------------------------------------------------------- +-- EXPORTS +-------------------------------------------------------------------------------- + +local exports = Window + +if __TEST__ then + exports.__private__ = { + WindowItem = WindowItem, + } +end + +return exports diff --git a/tests/collectors_spec.lua b/tests/collectors_spec.lua index b099f77..9977582 100644 --- a/tests/collectors_spec.lua +++ b/tests/collectors_spec.lua @@ -2,170 +2,201 @@ require('tests.bootstrap')(assert) describe('collectors', function() - local collectors - - setup(function() - collectors = require 'nginx-metrix.collectors'; - end) - - teardown(function() - package.loaded['nginx-metrix.collectors'] = nil - end) - - after_each(function() - collectors.__private__.collectors({}) - end) - - it('collector_exists', function() - assert.is_false(collectors.__private__.collector_exists({name='test-collector'})) - - collectors.__private__.collectors({{name='test-collector'}}) - - assert.is_true(collectors.__private__.collector_exists({name='test-collector'})) + local collectors + + setup(function() + collectors = require 'nginx-metrix.collectors'; + end) + + teardown(function() + package.loaded['nginx-metrix.collectors'] = nil + end) + + after_each(function() + collectors.__private__.collectors({}) + end) + + it('collector_exists', function() + assert.is_false(collectors.__private__.collector_exists({ name = 'test-collector' })) + + collectors.__private__.collectors({ { name = 'test-collector' } }) + + assert.is_true(collectors.__private__.collector_exists({ name = 'test-collector' })) + end) + + local validate_data_provider = { + { + collector = nil, + error = 'Collector MUST be a table, got nil: nil', + }, + { + collector = 'invalid collector type', + error = 'Collector MUST be a table, got string: "invalid collector type"', + }, + { + collector = {}, + error = 'Collector must have string property "name", got nil: {}', + }, + { + collector = { name = 13 }, + error = "Collector must have string property \"name\", got number: {\n name = 13\n}", + }, + { + collector = { name = 'existent-collector' }, + error = 'Collector already exists', + }, + { + collector = { + name = 'test-collector', + ngx_phases = nil, + }, + error = 'Collector.ngx_phases must be an array, given: nil', + }, + { + collector = { + name = 'test-collector', + ngx_phases = 'invalid type', + }, + error = 'Collector.ngx_phases must be an array, given: string', + }, + { + collector = { + name = 'test-collector', + ngx_phases = { [[valid phase]], 13 }, + }, + error = 'Collector.ngx_phases must be an array of strings, given: { "valid phase", 13 }', + }, + { + collector = { + name = 'test-collector', + ngx_phases = { [[valid phase]] }, + on_phase = 'invalid type', + }, + error = 'Collector:on_phase must be a function or callable table, given: string', + }, + { + collector = { + name = 'test-collector', + ngx_phases = { [[valid phase]] }, + on_phase = function() end, + fields = 'invalid type', + }, + error = 'Collector.fields must be a table, given: string', + }, + { + collector = { + name = 'test-collector', + ngx_phases = { [[valid phase]] }, + on_phase = function() end, + fields = { testfield = 'invalid type' }, + }, + error = "Collector.fields must be an table[string, table], given: {\n testfield = \"invalid type\"\n}", + }, + } + + for k, data in pairs(validate_data_provider) do + it('collector_validate failed #' .. k, function() + collectors.__private__.collectors({ { name = 'existent-collector' } }) + + assert.has_error(function() + collectors.__private__.collector_validate(data.collector) + end, + data.error) end) - - local validate_data_provider = { - { - collector = nil, - error = 'Collector MUST be a table, got nil: nil', - }, - { - collector = 'invalid collector type', - error = 'Collector MUST be a table, got string: "invalid collector type"', - }, - { - collector = {}, - error = 'Collector must have string property "name", got nil: {}', - }, - { - collector = {name = 13}, - error = "Collector must have string property \"name\", got number: {\n name = 13\n}", - }, - { - collector = {name = 'existent-collector'}, - error = 'Collector already exists', - }, - { - collector = { - name = 'test-collector', - ngx_phases = nil, - }, - error = 'Collector.ngx_phases must be an array, given: nil', - }, - { - collector = { - name = 'test-collector', - ngx_phases = 'invalid type', - }, - error = 'Collector.ngx_phases must be an array, given: string', - }, - { - collector = { - name = 'test-collector', - ngx_phases = {[[valid phase]], 13}, - }, - error = 'Collector.ngx_phases must be an array of strings, given: { "valid phase", 13 }', - }, - { - collector = { - name = 'test-collector', - ngx_phases = {[[valid phase]]}, - on_phase = 'invalid type', - }, - error = 'Collector:on_phase must be a function or callable table, given: string', - }, - { - collector = { - name = 'test-collector', - ngx_phases = {[[valid phase]]}, - on_phase = function() end, - fields = 'invalid type', - }, - error = 'Collector.fields must be a table, given: string', - }, - { - collector = { - name = 'test-collector', - ngx_phases = {[[valid phase]]}, - on_phase = function() end, - fields = {testfield = 'invalid type'}, - }, - error = "Collector.fields must be an table[string, table], given: {\n testfield = \"invalid type\"\n}", - }, + end + + it('collector_validate', function() + local test_collector = { + name = 'test-collector', + ngx_phases = { [[valid phase]] }, + on_phase = function() end, + fields = { testfield = {} }, } - for k, data in pairs(validate_data_provider) do - it('collector_validate failed #' .. k, function() - collectors.__private__.collectors({{name = 'existent-collector'}}) - - assert.has_error( - function() - collectors.__private__.collector_validate(data.collector) - end, - data.error - ) - end) - end - - it('collector_validate', function() - local test_collector = { - name = 'test-collector', - ngx_phases = {[[valid phase]]}, - on_phase = function() end, - fields = {testfield = {}}, - } - - assert.has_no.errors( - function() - collectors.__private__.collector_validate(test_collector) - end - ) - end) - - it('collector_extend', function() - local test_collector = { - name = 'test-collector', - ngx_phases = {[[valid phase]]}, - on_phase = function() end, - fields = {testfield = {}}, - } - - local extended_test_collector = collectors.__private__.collector_extend(test_collector) - - assert.is_same(test_collector, extended_test_collector) - - local metatable = getmetatable(extended_test_collector) - assert.is_table(metatable) - assert.is_table(metatable.__index) - assert.is_equal(metatable, metatable.__index) - assert.is_function(metatable.init) - assert.is_function(metatable.handle_ngx_phase) - assert.is_function(metatable.aggregate) - assert.is_function(metatable.get_raw_stats) - assert.is_function(metatable.get_text_stats) - assert.is_function(metatable.get_html_stats) + assert.has_no.errors(function() + collectors.__private__.collector_validate(test_collector) end) + end) + + it('collector_extend', function() + local storage_mock = mock({ + get = function() return 7 end, + mean_flush = function() end, + cyclic_flush = function() end, + }) + + local test_collector = { + name = 'test-collector', + ngx_phases = { [[valid phase]] }, + on_phase = function() end, + fields = { + testfield_mean = { mean = true }, + testfield_cyclic = { cyclic = true }, + }, + } - it('register', function() - local test_collector = { - name = 'test-collector', - ngx_phases = {[[valid phase]]}, - on_phase = function() end, - fields = {testfield = {}}, - } + local extended_test_collector = mock(collectors.__private__.collector_extend(test_collector)) + + assert.is_same(test_collector, extended_test_collector) + + local metatable = getmetatable(extended_test_collector) + assert.is_table(metatable) + assert.is_table(metatable.__index) + assert.is_equal(metatable, metatable.__index) + assert.is_function(metatable.init) + assert.is_function(metatable.handle_ngx_phase) + assert.is_function(metatable.aggregate) + assert.is_function(metatable.get_raw_stats) + assert.is_function(metatable.get_text_stats) + assert.is_function(metatable.get_html_stats) + + extended_test_collector:init(storage_mock) + + extended_test_collector:aggregate() + assert.spy(storage_mock.mean_flush).was.called(1) + assert.spy(storage_mock.cyclic_flush).was.called(1) + + local raw_stats = extended_test_collector:get_raw_stats():tomap() + assert.spy(storage_mock.get).was.called_with(storage_mock, 'testfield_mean') + assert.spy(storage_mock.get).was.called_with(storage_mock, 'testfield_cyclic') + assert.spy(storage_mock.get).was.called(2) + assert.is_same({ testfield_mean = 7, testfield_cyclic = 7, }, raw_stats) + + local text_output_helper = mock({render_stats = function() end}, true) + text_output_helper.render_stats.on_call_with(extended_test_collector).returns('rendered text stats') + local text_stats = extended_test_collector:get_text_stats(text_output_helper) + assert.spy(text_output_helper.render_stats).was.called_with(extended_test_collector) + assert.spy(text_output_helper.render_stats).was.called(1) + assert.is_equal('rendered text stats', text_stats) + + local html_output_helper = mock({render_stats = function() end}, true) + html_output_helper.render_stats.on_call_with(extended_test_collector).returns('rendered html stats') + local html_stats = extended_test_collector:get_html_stats(html_output_helper) + assert.spy(html_output_helper.render_stats).was.called_with(extended_test_collector) + assert.spy(html_output_helper.render_stats).was.called(1) + assert.is_equal('rendered html stats', html_stats) + end) + + it('register', function() + local test_collector = { + name = 'test-collector', + ngx_phases = { [[valid phase]] }, + on_phase = function() end, + fields = { testfield = {} }, + } - local extended_test_collector = collectors.register(test_collector) - assert.is_same(test_collector, extended_test_collector) + local extended_test_collector = collectors.register(test_collector) + assert.is_same(test_collector, extended_test_collector) - assert.is_same({test_collector}, collectors.__private__.collectors()) - end) + assert.is_same({ test_collector }, collectors.__private__.collectors()) + end) - it('all', function() - local test_collectors = {{name = 'existent-collector'}} - collectors.__private__.collectors(test_collectors) + it('all', function() + local test_collectors = { { name = 'existent-collector' } } + collectors.__private__.collectors(test_collectors) - assert.is_table(collectors.all) - assert.is_function(collectors.all.totable) - assert.is_same(test_collectors, collectors.all:totable{}) - end) + assert.is_table(collectors.all) + assert.is_function(collectors.all.totable) + assert.is_same(test_collectors, collectors.all:totable {}) + end) end) diff --git a/tests/main_spec.lua b/tests/main_spec.lua index 8eab8ed..c163605 100644 --- a/tests/main_spec.lua +++ b/tests/main_spec.lua @@ -135,6 +135,7 @@ describe('main', function() stub(ngx, 'say') metrix.show({vhosts_filter='.*'}) + metrix.handle_ngx_phase('log') assert.spy(ngx.say).was.called(1) assert.spy(ngx.say).was.called_with(match.json_equal({ ['service'] = "nginx-metrix", @@ -147,6 +148,7 @@ describe('main', function() stub(ngx, 'say') metrix.show({vhosts_filter='first.com'}) + metrix.handle_ngx_phase('log') assert.spy(ngx.say).was.called(1) assert.spy(ngx.say).was.called_with(match.json_equal({ ['service'] = "nginx-metrix", @@ -154,5 +156,8 @@ describe('main', function() ['vhost'] = "first.com", })) ngx.say:revert() + + metrix.handle_ngx_phase('log') + end) end) diff --git a/tests/scheduler_spec.lua b/tests/scheduler_spec.lua index 7a93c58..06a1779 100644 --- a/tests/scheduler_spec.lua +++ b/tests/scheduler_spec.lua @@ -22,7 +22,10 @@ describe('scheduler', function() spawn = function() end, wait = function() end, }, - now = function() return os.time() end, + worker = { + id = function() return 13 end, + pid = function() return 113 end, + }, } end) @@ -94,19 +97,9 @@ describe('scheduler', function() assert.is_equal(1, length(scheduler.__private__.collectors())) end) - it('start failed because worker_id', function() - local worker_id = 13 - dict_mock.safe_incr.on_call_with('worker_id').returns(nil, 'test error') - - assert.is_false(scheduler.start()) - assert.spy(_G.ngx.timer.at).was_not.called() - assert.spy(logger.error).was.called_with('Can not make worker_id', 'test error') - assert.spy(logger.error).was.called(1) - end) - it('start failed because timer', function() local worker_id = 13 - dict_mock.safe_incr.on_call_with('worker_id').returns(worker_id, nil) + stub.new(_G.ngx.worker, 'id').on_call_with().returns(worker_id) stub.new(_G.ngx.timer, 'at').on_call_with(1, match.is_function(), {}, worker_id).returns(false, 'test error') assert.is_false(scheduler.start()) @@ -118,7 +111,7 @@ describe('scheduler', function() it('start without collectors', function() local worker_id = 13 - dict_mock.safe_incr.on_call_with('worker_id').returns(worker_id, nil) + stub.new(_G.ngx.worker, 'id').on_call_with().returns(worker_id) stub.new(_G.ngx.timer, 'at').on_call_with(1, match.is_function(), {}, worker_id).returns(true, nil) assert.is_true(scheduler.start()) @@ -131,7 +124,7 @@ describe('scheduler', function() scheduler.attach_collector(test_collector) local worker_id = 13 - dict_mock.safe_incr.on_call_with('worker_id').returns(worker_id, nil) + stub.new(_G.ngx.worker, 'id').on_call_with().returns(worker_id) stub.new(_G.ngx.timer, 'at').on_call_with(1, match.is_function(), { test_collector }, worker_id).returns(true, nil) assert.is_true(scheduler.start()) @@ -141,7 +134,7 @@ describe('scheduler', function() end) it('handler on premature call', function() - local process_stub = mock({process = function() end}).process + local process_stub = mock({ process = function() end }).process scheduler.__private__._process(process_stub) scheduler.__private__.handler(true, {}, 1) @@ -153,7 +146,7 @@ describe('scheduler', function() it('handler failed to start next iter', function() local worker_id = 13 - local process_stub = mock({process = function() end}).process + local process_stub = mock({ process = function() end }).process scheduler.__private__._process(process_stub) namespaces.list.on_call_with().returns({}) @@ -193,7 +186,7 @@ describe('scheduler', function() dict_mock.add.on_call_with(scheduler.__private__.lock_key(), scheduler.__private__.lock_key(), scheduler.__private__.lock_timeout()).returns(false, 'exists', false) - scheduler.__private__.process({[[collector]]}, {'example.com'}, worker_id) + scheduler.__private__.process({ [[collector]] }, { 'example.com' }, worker_id) assert.spy(dict_mock.add).was.called_with(scheduler.__private__.lock_key(), scheduler.__private__.lock_key(), scheduler.__private__.lock_timeout()) assert.spy(dict_mock.add).was.called(1) @@ -214,8 +207,6 @@ describe('scheduler', function() stub.new(_G.ngx.thread, 'wait').on_call_with(thread).returns(false, 'test error') stub.new(_G.ngx.timer, 'at').on_call_with(1, match.is_function(), { test_collector }, {'example.com'} , worker_id).returns(true, nil) --- logger.error.on_call_with(match._, match._).invokes(function(...) print(require 'inspect'({...})) end) - scheduler.__private__.process({ test_collector }, {'example.com'}, worker_id) assert.spy(dict_mock.add).was.called_with(scheduler.__private__.lock_key(), scheduler.__private__.lock_key(), scheduler.__private__.lock_timeout()) @@ -240,8 +231,6 @@ describe('scheduler', function() stub.new(_G.ngx.thread, 'spawn').on_call_with(match.is_function()).invokes(function(func) thread.func = func; return thread end) stub.new(_G.ngx.thread, 'wait').on_call_with(match._).invokes(function(thread) thread.func(); return true, nil end) - logger.error.on_call_with(match._, match._).invokes(function(...) print(require 'inspect'({...})) end) - scheduler.__private__.process({ test_collector }, {'first.com', 'second.org'}, worker_id) assert.spy(dict_mock.add).was.called_with(scheduler.__private__.lock_key(), scheduler.__private__.lock_key(), scheduler.__private__.lock_timeout()) diff --git a/tests/storage/collector_wrapper_factory_spec.lua b/tests/storage/collector_wrapper_factory_spec.lua index cc9765f..34870a6 100644 --- a/tests/storage/collector_wrapper_factory_spec.lua +++ b/tests/storage/collector_wrapper_factory_spec.lua @@ -1,10 +1,12 @@ require('tests.bootstrap')(assert) describe('storage.collector_wrapper_factory', function() + local match local namespaces local dict_mock local factory + local Window local mk_wrapper_metatable_mock = function(name) local wrapper_metatable = copy(factory.__private__.wrapper_metatable) @@ -15,6 +17,8 @@ describe('storage.collector_wrapper_factory', function() end setup(function() + match = require 'luassert.match' + package.loaded['nginx-metrix.storage.dict'] = nil dict_mock = mock(require 'nginx-metrix.storage.dict', true) package.loaded['nginx-metrix.storage.dict'] = dict_mock @@ -22,6 +26,9 @@ describe('storage.collector_wrapper_factory', function() package.loaded['nginx-metrix.storage.namespaces'] = nil namespaces = require 'nginx-metrix.storage.namespaces' + package.loaded['nginx-metrix.storage.window'] = nil + Window = require 'nginx-metrix.storage.window' + package.loaded['nginx-metrix.storage.collector_wrapper_factory'] = nil factory = require 'nginx-metrix.storage.collector_wrapper_factory' end) @@ -29,6 +36,7 @@ describe('storage.collector_wrapper_factory', function() teardown(function() mock.revert(dict_mock) package.loaded['nginx-metrix.storage.dict'] = nil + package.loaded['nginx-metrix.storage.window'] = nil package.loaded['nginx-metrix.storage.collector_wrapper_factory'] = nil end) @@ -36,6 +44,7 @@ describe('storage.collector_wrapper_factory', function() after_each(function() namespaces.reset_active() mock.clear(dict_mock) + _G.ngx = nil end) it('create', function() @@ -240,4 +249,93 @@ describe('storage.collector_wrapper_factory', function() dict_mock.get:revert() end) + + it('wrapper_metatable.cyclic_flush [non existent, window]', function() + factory.set_window_size(10) + + local wrapper_metatable = mk_wrapper_metatable_mock('collector-mock') + + stub.new(dict_mock, 'get').on_call_with('collector_mock¦test-key^^next^^').returns(nil, 0) + local sik = stub.new(Window.__private__.WindowItem, 'create_key') + sik.on_call_with().returns('11111111111111111111111111111111') + + wrapper_metatable:cyclic_flush('test-key', true) + assert.spy(dict_mock.get).was.called_with('collector_mock¦test-key^^next^^') + assert.spy(dict_mock.delete).was.called_with('collector_mock¦test-key^^next^^') + assert.spy(dict_mock.set).was.called_with('collector_mock¦test-key', 0, 0, 0) + + dict_mock.get:revert() + sik:revert() + end) + + it('wrapper_metatable.cyclic_flush [existent, window]', function() + factory.set_window_size(10) + + local wrapper_metatable = mk_wrapper_metatable_mock('collector-mock') + + local test_data_wi = { + ['11111111111111111111111111111111'] = { + _key = '11111111111111111111111111111111', + _payload = 7, + }, + ['22222222222222222222222222222222'] = { + _key = '22222222222222222222222222222222', + _payload = 13, + }, + } + + local test_data = { + { + wikey = '11111111111111111111111111111111', + window = nil, + }, + { + wikey = '22222222222222222222222222222222', + window = { + _head = "11111111111111111111111111111111", + _limit = 10, + _name = "collector_mock¦test-key", + _size = 1, + _tail = "11111111111111111111111111111111", + }, + }, + } + + local test_data_index; + + _G.ngx = mock({ now = function() end, md5 = function() end }, true) + _G.ngx.now.on_call_with().returns(os.time()) + _G.ngx.md5.on_call_with(match._).invokes(function() + return test_data[test_data_index].wikey + end) + + -- stub.new(dict_mock, 'get').on_call_with('collector_mock¦test-key^^next^^').returns(7) + stub.new(dict_mock, 'get').on_call_with(match._).invokes(function(key) + local ret_val + if key == 'collector_mock¦test-key^^next^^' then + ret_val = test_data_wi[test_data[test_data_index].wikey]._payload + elseif key:match('^WindowItem') then + ret_val = test_data_wi[key:sub(12)] + elseif key:match('^Window') then + ret_val = test_data[test_data_index].window + end +-- print('dict.get(' .. key .. ') => ' .. require 'inspect'(ret_val, { depth = 1 })) + return ret_val + end) + stub.new(dict_mock, 'set').on_call_with(match._, match._).invokes(function(...) +-- print('dict.set(' .. require 'inspect'({ ... }, { depth = 2 }) .. ')') + end) + + test_data_index = 1 + wrapper_metatable:cyclic_flush('test-key', true) + test_data_index = 2 + wrapper_metatable:cyclic_flush('test-key', true) + + assert.spy(dict_mock.get).was.called_with('collector_mock¦test-key^^next^^') + assert.spy(dict_mock.delete).was.called_with('collector_mock¦test-key^^next^^') + assert.spy(dict_mock.set).was.called_with('collector_mock¦test-key', 7, 0, 0) + assert.spy(dict_mock.set).was.called_with('collector_mock¦test-key', 10, 0, 0) + + dict_mock.get:revert() + end) end) diff --git a/tests/storage/dict_spec.lua b/tests/storage/dict_spec.lua index 884f71b..0dddac8 100644 --- a/tests/storage/dict_spec.lua +++ b/tests/storage/dict_spec.lua @@ -244,11 +244,19 @@ describe('storage.dict', function() stub.new(shared_dict, 'incr').on_call_with(shared_dict, test_key, 2).returns(3, nil) newval, err = dict.safe_incr(test_key, 2) assert.spy(shared_dict.incr).was.called_with(shared_dict, test_key, 2) + assert.spy(shared_dict.incr).was.called(1) + assert.spy(shared_dict.add).was_not.called() assert.are.equal(3, newval) assert.is_nil(err) + mock.clear(shared_dict) + stub.new(shared_dict, 'incr').on_call_with(match._, test_key, 13).returns(nil, 'unhandled error') + newval, err = dict.safe_incr(test_key, 13) + assert.spy(shared_dict.incr).was.called_with(shared_dict, test_key, 13) assert.spy(shared_dict.incr).was.called(1) assert.spy(shared_dict.add).was_not.called() + assert.is_nil(newval) + assert.are.equal('unhandled error', err) mock.clear(shared_dict) end) diff --git a/tests/storage/window_spec.lua b/tests/storage/window_spec.lua new file mode 100644 index 0000000..181f66e --- /dev/null +++ b/tests/storage/window_spec.lua @@ -0,0 +1,394 @@ +require('tests.bootstrap')(assert) + +describe('storage.window', function() + local match + local dict_mock + local Window + + setup(function() + match = require 'luassert.match' + + package.loaded['nginx-metrix.storage.dict'] = nil + dict_mock = mock(require 'nginx-metrix.storage.dict', true) + package.loaded['nginx-metrix.storage.dict'] = dict_mock + end) + + teardown(function() + mock.revert(dict_mock) + package.loaded['nginx-metrix.storage.dict'] = nil + package.loaded['nginx-metrix.storage.window'] = nil + end) + + before_each(function() + Window = require 'nginx-metrix.storage.window' + end) + + after_each(function() + mock.clear(dict_mock) + dict_mock.get:revert() + stub.new(dict_mock, 'get') + dict_mock.set:revert() + stub.new(dict_mock, 'set') + _G.ngx = nil + package.loaded['nginx-metrix.storage.window'] = nil + end) + + -- Window Item -- + it('WindowItem create key', function() + _G.ngx = mock({ now = function() end, md5 = function() end }, true) + _G.ngx.now.on_call_with().returns(os.time()) + _G.ngx.md5.on_call_with(match._).returns('0123456789AbCdEf0123456789aBcDeF') + + local key = Window.__private__.WindowItem.create_key('payload') + assert.spy(_G.ngx.now).was.called(2) + assert.spy(_G.ngx.md5).was.called_with(match.has_match('^%d+%.%d+ %d+%.%d+$')) + assert.spy(_G.ngx.md5).was.called(1) + assert.is_equals(32, key:len()) + assert.matches('^[a-fA-F0-9]+$', key) + end) + + it('WindowItem new', function() + local sk = stub.new(Window.__private__.WindowItem, 'create_key') + local ss = stub.new(Window.__private__.WindowItem, 'store') + + sk.on_call_with().returns('0123456789AbCdEf0123456789aBcDeF') + + local windowItem = Window.__private__.WindowItem.new('payload') + + assert.spy(sk).was.called(1) + assert.spy(ss).was.called_with(windowItem) + assert.spy(ss).was.called(1) + + assert.is_table(windowItem) + + assert.is_string(windowItem._key) + assert.is_equals(32, windowItem._key:len()) + assert.matches('^[a-fA-F0-9]+$', windowItem._key) + + assert.is_equals('payload', windowItem._payload) + + assert.is_nil(windowItem._next) + + assert.is_table(getmetatable(windowItem)) + assert.is_equals(Window.__private__.WindowItem, getmetatable(windowItem)) + + sk:revert() + ss:revert() + end) + + it('WindowItem store', function() + local sk = stub.new(Window.__private__.WindowItem, 'create_key') + + sk.on_call_with().returns('0123456789AbCdEf0123456789aBcDeF') + + local windowItem = Window.__private__.WindowItem.new('payload') + + assert.spy(dict_mock.set).was.called_with('WindowItem^0123456789AbCdEf0123456789aBcDeF', windowItem) + assert.spy(dict_mock.set).was.called(1) + + sk:revert() + end) + + it('WindowItem restore', function() + dict_mock.get.on_call_with('WindowItem^NINEXISTENTITEMKEY').returns(nil, 0) + local windowItem = Window.__private__.WindowItem.restore('NINEXISTENTITEMKEY') + assert.spy(dict_mock.get).was.called_with('WindowItem^NINEXISTENTITEMKEY') + assert.is_nil(windowItem) + + dict_mock.get.on_call_with('WindowItem^0123456789AbCdEf0123456789aBcDeF').returns({ _key = '0123456789AbCdEf0123456789aBcDeF', _payload = 'payload', _next = nil }) + local windowItem = Window.__private__.WindowItem.restore('0123456789AbCdEf0123456789aBcDeF') + assert.spy(dict_mock.get).was.called_with('WindowItem^0123456789AbCdEf0123456789aBcDeF') + assert.is_table(windowItem) + assert.is_equals('0123456789AbCdEf0123456789aBcDeF', windowItem._key) + assert.is_equals('payload', windowItem._payload) + assert.is_nil(windowItem._next) + assert.is_table(getmetatable(windowItem)) + + assert.spy(dict_mock.get).was.called(2) + end) + + it('WindowItem key getter', function() + local sk = stub.new(Window.__private__.WindowItem, 'create_key') + local ss = stub.new(Window.__private__.WindowItem, 'store') + + sk.on_call_with().returns('0123456789AbCdEf0123456789aBcDeF') + + local windowItem = Window.__private__.WindowItem.new('payload') + + assert.spy(sk).was.called(1) + assert.spy(ss).was.called_with(windowItem) + assert.spy(ss).was.called(1) + + assert.is_table(windowItem) + + assert.is_equals('0123456789AbCdEf0123456789aBcDeF', windowItem:key()) + + sk:revert() + ss:revert() + end) + + it('WindowItem payload getter', function() + local sk = stub.new(Window.__private__.WindowItem, 'create_key') + local ss = stub.new(Window.__private__.WindowItem, 'store') + + sk.on_call_with().returns('0123456789AbCdEf0123456789aBcDeF') + + local windowItem = Window.__private__.WindowItem.new('payload') + + assert.spy(sk).was.called(1) + assert.spy(ss).was.called_with(windowItem) + assert.spy(ss).was.called(1) + + assert.is_table(windowItem) + + assert.is_equals('payload', windowItem:payload()) + + sk:revert() + ss:revert() + end) + + it('WindowItem next getter and setter', function() + local sk = stub.new(Window.__private__.WindowItem, 'create_key') + local ss = stub.new(Window.__private__.WindowItem, 'store') + local sr = stub.new(Window.__private__.WindowItem, 'restore') + + sk.on_call_with().returns('11111111111111111111111111111111') + local windowItem1 = Window.__private__.WindowItem.new('payload 1') + assert.spy(ss).was.called_with(windowItem1) + + assert.is_nil(windowItem1:next()) + + sk.on_call_with().returns('22222222222222222222222222222222') + local windowItem2 = Window.__private__.WindowItem.new('payload 2') + assert.spy(ss).was.called_with(windowItem2) + + windowItem1:next(windowItem2) + assert.spy(ss).was.called_with(windowItem1) + assert.is_equals(windowItem2._key, windowItem1._next) + + sr.on_call_with(windowItem2._key).returns(windowItem2) + local nextItem = windowItem1:next() + assert.spy(sr).was.called_with(windowItem2._key) + assert.is_same(windowItem2, nextItem) + + assert.spy(sk).was.called(2) + assert.spy(ss).was.called(3) + assert.spy(sr).was.called(1) + + sk:revert() + ss:revert() + end) + -- Window Item -- + + -- Window -- + it('Window open new', function() + dict_mock.get.on_call_with('Window^test-window').returns(nil) + + local window = Window.open('test-window', 3) + assert.is_table(window) + assert.is_equals('test-window', window._name) + assert.is_equals(3, window._limit) + assert.is_equals(0, window._size) + assert.is_nil(window._head) + assert.is_nil(window._tail) + assert.is_table(getmetatable(window)) + assert.is_equals(Window, getmetatable(window)) + end) + + it('Window open existent', function() + dict_mock.get.on_call_with('Window^test-window').returns({ _name = 'test-window', _limit = 4, _size = 2, _head = '11111111111111111111111111111111', _tail = '22222222222222222222222222222222' }) + + local window = Window.open('test-window', 3) + assert.is_table(window) + assert.is_equals('test-window', window._name) + assert.is_equals(4, window._limit) + assert.is_equals(2, window._size) + assert.is_equals('11111111111111111111111111111111', window._head) + assert.is_equals('22222222222222222222222222222222', window._tail) + assert.is_table(getmetatable(window)) + assert.is_equals(Window, getmetatable(window)) + end) + + it('Window store', function() + dict_mock.get.on_call_with('Window^test-window').returns({ _name = 'test-window', _limit = 4, _size = 2, _head = '11111111111111111111111111111111', _tail = '22222222222222222222222222222222' }) + + local window = Window.open('test-window', 3) + window:store() + + assert.spy(dict_mock.set).was.called_with('Window^test-window', window) + assert.spy(dict_mock.set).was.called(1) + end) + + it('Window push without displacement #1', function() + dict_mock.get.on_call_with('Window^test-window').returns(nil) + local sik = stub.new(Window.__private__.WindowItem, 'create_key') + local sis = stub.new(Window.__private__.WindowItem, 'store') + local sir = stub.new(Window.__private__.WindowItem, 'restore') + local sqs = stub.new(Window, 'store') + local sqp = stub.new(Window, 'pop') + + sik.on_call_with().returns('0123456789AbCdEf0123456789aBcDeF') + + local window = Window.open('test-window', 2) + + window:push(1) + + assert.spy(sqs).was.called_with(window) + assert.spy(sqp).was_not.called() + assert.spy(sir).was_not.called() + assert.is_equals(1, window:len()) + + sik:revert() + sis:revert() + sir:revert() + sqs:revert() + sqp:revert() + end) + + it('Window push without displacement #2', function() + dict_mock.get.on_call_with('Window^test-window').returns({ _name = 'test-window', _limit = 2, _size = 1, _head = '11111111111111111111111111111111', _tail = '11111111111111111111111111111111' }) + + local sis = stub.new(Window.__private__.WindowItem, 'store') + local sir = stub.new(Window.__private__.WindowItem, 'restore') + local sqs = stub.new(Window, 'store') + local sqp = stub.new(Window, 'pop') + + local sik = stub.new(Window.__private__.WindowItem, 'create_key') + sik.on_call_with().returns('11111111111111111111111111111111') + local item_mock = Window.__private__.WindowItem.new('payload') + spy.on(item_mock, 'next') + sik:revert(); + + local sik = stub.new(Window.__private__.WindowItem, 'create_key') + sir.on_call_with('11111111111111111111111111111111').returns(item_mock) + + sik.on_call_with().returns('0123456789AbCdEf0123456789aBcDeF') + + local window = Window.open('test-window', 2) + + window:push(1) + + assert.spy(sqs).was.called_with(window) + assert.spy(sqp).was_not.called() + assert.spy(sir).was.called_with('11111111111111111111111111111111') + assert.spy(sir).was.called(1) + assert.spy(item_mock.next).was.called(1) + assert.is_equals(2, window:len()) + assert.is_equals('0123456789AbCdEf0123456789aBcDeF', window._tail) + + sik:revert() + sis:revert() + sir:revert() + sqs:revert() + sqp:revert() + end) + + it('Window push with displacement', function() + dict_mock.get.on_call_with('Window^test-window').returns(nil) + local window = Window.open('test-window', 2) + + local sik = stub.new(Window.__private__.WindowItem, 'create_key') + local sis = stub.new(Window.__private__.WindowItem, 'store') + local sir = stub.new(Window.__private__.WindowItem, 'restore') + local sqs = stub.new(window, 'store') + local sqp = spy.on(window, 'pop') + + local key_gen_count = 0; + sik.on_call_with().invokes(function() + key_gen_count = key_gen_count + 1 + return '0000000000000000000000000000000' .. key_gen_count + end) + + local items = { + ['00000000000000000000000000000001'] = mock(Window.__private__.WindowItem.new('payload 1')), + ['00000000000000000000000000000002'] = mock(Window.__private__.WindowItem.new('payload 2')), + ['00000000000000000000000000000003'] = mock(Window.__private__.WindowItem.new('payload 3')), + } + key_gen_count = 0; + + sir.on_call_with(match._).invokes(function(key) + return items[key] + end) + + window:push(1) + window:push(2) + window:push(3) + + assert.spy(sqs).was.called_with(window) + assert.spy(sqs).was.called(4) + assert.spy(window.pop).was.called(1) + assert.is_equals(2, window:len()) + + sik:revert() + sis:revert() + sir:revert() + sqs:revert() + sqp:revert() + end) + + it('Window pop', function() + dict_mock.get.on_call_with('Window^test-window').returns({ _name = 'test-window', _limit = 2, _size = 1, _head = '11111111111111111111111111111111', _tail = '11111111111111111111111111111111' }) + + local sik = stub.new(Window.__private__.WindowItem, 'create_key') + local sir = stub.new(Window.__private__.WindowItem, 'restore') + local sqs = stub.new(Window, 'store') + sik.on_call_with().returns('11111111111111111111111111111111') + + local item_mock = mock(Window.__private__.WindowItem.new('payload')) + sir.on_call_with('11111111111111111111111111111111').returns(item_mock) + + local window = Window.open('test-window', 2) + + local value = window:pop() + + assert.spy(sir).was.called_with('11111111111111111111111111111111') + assert.spy(sir).was.called(1) + assert.spy(sqs).was.called_with(window) + assert.spy(sqs).was.called(1) + assert.is_equals(0, window:len()) + assert.is_equals('payload', value) + assert.is_nil(window._head) + assert.is_nil(window._tail) + + sik:revert() + sir:revert() + sqs:revert() + end) + + it('Window pop from empty', function() + dict_mock.get.on_call_with('Window^test-window').returns(nil) + local window = Window.open('test-window', 2) + + local item = window:pop() + + assert.is_nil(item) + end) + + it('Window totable empty', function() + dict_mock.get.on_call_with('Window^test-window').returns(nil) + local window = Window.open('test-window', 2) + + local list = window:totable() + + assert.is_same({}, list) + end) + + it('Window totable not empty', function() + dict_mock.get.on_call_with('Window^test-window').returns({ _name = 'test-window', _limit = 2, _size = 1, _head = '11111111111111111111111111111111', _tail = '11111111111111111111111111111111' }) + local window = Window.open('test-window', 2) + + local sik = stub.new(Window.__private__.WindowItem, 'create_key') + sik.on_call_with().returns('11111111111111111111111111111111') + local item_mock = mock(Window.__private__.WindowItem.new('payload')) + local sir = stub.new(Window.__private__.WindowItem, 'restore') + sir.on_call_with('11111111111111111111111111111111').returns(item_mock) + + local list = window:totable() + + assert.is_same({'payload'}, list) + + sik:revert() + sir:revert() + end) + -- /Window -- +end)